storage.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import os
  2. import csv
  3. import json
  4. import time
  5. try:
  6. import cPickle as pickle
  7. except ImportError:
  8. import pickle
  9. import shutil
  10. import collections
  11. from datetime import datetime
  12. class _PersistentDictMixin(object):
  13. def __init__(self, filename, flag='c', mode=None, file_format='pickle'):
  14. self.flag = flag # r=readonly, c=create, or n=new
  15. self.mode = mode # None or an octal triple like 0644
  16. self.file_format = file_format # 'csv', 'json', or 'pickle'
  17. self.filename = filename
  18. if flag != 'n' and os.access(filename, os.R_OK):
  19. fileobj = open(filename, 'rb' if file_format == 'pickle' else 'r')
  20. with fileobj:
  21. self.load(fileobj)
  22. def sync(self):
  23. '''Write the dict to disk'''
  24. if self.flag == 'r':
  25. return
  26. filename = self.filename
  27. tempname = filename + '.tmp'
  28. fileobj = open(tempname, 'wb' if self.file_format == 'pickle' else 'w')
  29. try:
  30. self.dump(fileobj)
  31. except Exception:
  32. os.remove(tempname)
  33. raise
  34. finally:
  35. fileobj.close()
  36. shutil.move(tempname, self.filename) # atomic commit
  37. if self.mode is not None:
  38. os.chmod(self.filename, self.mode)
  39. def close(self):
  40. '''Calls sync'''
  41. self.sync()
  42. def __enter__(self):
  43. return self
  44. def __exit__(self, *exc_info):
  45. self.close()
  46. def dump(self, fileobj):
  47. '''Handles the writing of the dict to the file object'''
  48. if self.file_format == 'csv':
  49. csv.writer(fileobj).writerows(self.raw_dict().items())
  50. elif self.file_format == 'json':
  51. json.dump(self.raw_dict(), fileobj, separators=(',', ':'))
  52. elif self.file_format == 'pickle':
  53. pickle.dump(dict(self.raw_dict()), fileobj, 2)
  54. else:
  55. raise NotImplementedError('Unknown format: ' +
  56. repr(self.file_format))
  57. def load(self, fileobj):
  58. '''Load the dict from the file object'''
  59. # try formats from most restrictive to least restrictive
  60. for loader in (pickle.load, json.load, csv.reader):
  61. fileobj.seek(0)
  62. try:
  63. return self.initial_update(loader(fileobj))
  64. except Exception as e:
  65. pass
  66. raise ValueError('File not in a supported format')
  67. def raw_dict(self):
  68. '''Returns the underlying dict'''
  69. raise NotImplementedError
  70. class _Storage(collections.MutableMapping, _PersistentDictMixin):
  71. '''Storage that acts like a dict but also can persist to disk.
  72. :param filename: An absolute filepath to reprsent the storage on disk. The
  73. storage will loaded from this file if it already exists,
  74. otherwise the file will be created.
  75. :param file_format: 'pickle', 'json' or 'csv'. pickle is the default. Be
  76. aware that json and csv have limited support for python
  77. objets.
  78. .. warning:: Currently there are no limitations on the size of the storage.
  79. Please be sure to call :meth:`~xbmcswift2._Storage.clear`
  80. periodically.
  81. '''
  82. def __init__(self, filename, file_format='pickle'):
  83. '''Acceptable formats are 'csv', 'json' and 'pickle'.'''
  84. self._items = {}
  85. _PersistentDictMixin.__init__(self, filename, file_format=file_format)
  86. def __setitem__(self, key, val):
  87. self._items.__setitem__(key, val)
  88. def __getitem__(self, key):
  89. return self._items.__getitem__(key)
  90. def __delitem__(self, key):
  91. self._items.__delitem__(key)
  92. def __iter__(self):
  93. return iter(self._items)
  94. def __len__(self):
  95. return self._items.__len__
  96. def raw_dict(self):
  97. '''Returns the wrapped dict'''
  98. return self._items
  99. initial_update = collections.MutableMapping.update
  100. def clear(self):
  101. super(_Storage, self).clear()
  102. self.sync()
  103. class TimedStorage(_Storage):
  104. '''A dict with the ability to persist to disk and TTL for items.'''
  105. def __init__(self, filename, file_format='pickle', TTL=None):
  106. '''TTL if provided should be a datetime.timedelta. Any entries
  107. older than the provided TTL will be removed upon load and upon item
  108. access.
  109. '''
  110. self.TTL = TTL
  111. _Storage.__init__(self, filename, file_format=file_format)
  112. def __setitem__(self, key, val, raw=False):
  113. if raw:
  114. self._items[key] = val
  115. else:
  116. self._items[key] = (val, time.time())
  117. def __getitem__(self, key):
  118. val, timestamp = self._items[key]
  119. if self.TTL and (datetime.utcnow() -
  120. datetime.utcfromtimestamp(timestamp) > self.TTL):
  121. del self._items[key]
  122. return self._items[key][0] # Will raise KeyError
  123. return val
  124. def initial_update(self, mapping):
  125. '''Initially fills the underlying dictionary with keys, values and
  126. timestamps.
  127. '''
  128. for key, val in mapping.items():
  129. _, timestamp = val
  130. if not self.TTL or (datetime.utcnow() -
  131. datetime.utcfromtimestamp(timestamp) < self.TTL):
  132. self.__setitem__(key, val, raw=True)