storage.py 5.4 KB

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