Python Library Caching
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. """
  5. caching (Caching Module)
  6. ========================
  7. **Author:**
  8. * Dirk Alders <sudo-dirk@mount-mockery.de>
  9. **Description:**
  10. This Module supports functions and classes for caching e.g. properties of other instances.
  11. **Submodules:**
  12. * :class:`caching.property_cache_json`
  13. * :class:`caching.property_cache_pickle`
  14. **Unittest:**
  15. See also the :download:`unittest <caching/_testresults_/unittest.pdf>` documentation.
  16. """
  17. __DEPENDENCIES__ = []
  18. import json
  19. import logging
  20. import os
  21. import pickle
  22. import time
  23. try:
  24. from config import APP_NAME as ROOT_LOGGER_NAME
  25. except ImportError:
  26. ROOT_LOGGER_NAME = 'root'
  27. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  28. __DESCRIPTION__ = """The Module {\\tt %s} is designed to store information in {\\tt json} or {\\tt pickle} files to support them much faster then generating them from the original source file.
  29. For more Information read the documentation.""" % __name__.replace('_', '\_')
  30. """The Module Description"""
  31. __INTERPRETER__ = (3, )
  32. """The Tested Interpreter-Versions"""
  33. class property_cache_pickle(object):
  34. """
  35. This class caches the data from a given `source_instance`. It takes the data from the cache instead of generating the data from the `source_instance`,
  36. if the conditions for the cache usage are given.
  37. .. admonition:: Required properties for the `source_instance`
  38. * **uid():** returns the unique id of the source's source or None, if you don't want to use the unique id.
  39. * **keys():** returns a list of all available keys.
  40. * **data_version():** returns a version number of the current data (it should be increased, if the get method of the source instance returns improved values or the data structure had been changed).
  41. * **get(key, default):** returns the property for a key. If key does not exists, default will be returned.
  42. :param source_instance: The source instance holding the data
  43. :type source_instance: instance
  44. :param cache_filename: File name, where the properties are stored as cache
  45. :type cache_filename: str
  46. :param load_all_on_init: True will load all data from the source instance, when the cache will be initialised the first time.
  47. :type load_all_on_init: bool
  48. :param callback_on_data_storage: The callback will be executed every time when the cache file is stored. It will be executed with the instance of this class as first argument.
  49. :type callback_on_data_storage: method
  50. :param max_age: The maximum age of the cached data in seconds or None for no maximum age.
  51. :type max_age: int or None
  52. :param store_on_get: False will prevent cache storage with execution of the `.get(key, default)` method. You need to store the cache somewhere else.
  53. :type store_on_get: bool
  54. .. admonition:: The cache will be used, if all following conditions are given
  55. * The key is in the list returned by `.keys()` method of the `source_instance`
  56. * The key is not in the list of keys added by the `.add_source_get_keys()` method.
  57. * The cache age is less then the given max_age parameter or the given max_age is None.
  58. * The uid of the source instance (e.g. a checksum or unique id of the source) is identically to to uid stored in the cache.
  59. * The data version of the `source_instance` is <= the data version stored in the cache.
  60. * The value is available in the previous stored information
  61. **Example:**
  62. .. literalinclude:: caching/_examples_/property_cache_pickle.py
  63. Will result on the first execution to the following output (with a long execution time):
  64. .. literalinclude:: caching/_examples_/property_cache_pickle_1.log
  65. With every following execution the time cosumption my by much smaller:
  66. .. literalinclude:: caching/_examples_/property_cache_pickle_2.log
  67. """
  68. DATA_VERSION_TAG = '_property_cache_data_version_'
  69. STORAGE_VERSION_TAG = '_storage_version_'
  70. UID_TAG = '_property_cache_uid_'
  71. DATA_TAG = '_data_'
  72. AGE_TAG = '_age_'
  73. #
  74. STORAGE_VERSION = 1
  75. def __init__(self, source_instance, cache_filename, load_all_on_init=False, callback_on_data_storage=None, max_age=None, store_on_get=True, return_source_on_none=False):
  76. self._source_instance = source_instance
  77. self._cache_filename = cache_filename
  78. self._load_all_on_init = load_all_on_init
  79. self._callback_on_data_storage = callback_on_data_storage
  80. self._max_age = max_age
  81. self._store_on_get = store_on_get
  82. self._return_source_on_none = return_source_on_none
  83. #
  84. self._source_get_keys = []
  85. self._cached_props = None
  86. def add_source_get_keys(self, keys):
  87. """
  88. This will add one or more keys to a list of keys which will always be provided by the `source_instance` instead of the cache.
  89. :param keys: The key or keys to be added
  90. :type keys: list, tuple, str
  91. """
  92. if type(keys) in [list, tuple]:
  93. self._source_get_keys.extend(keys)
  94. else:
  95. self._source_get_keys.append(keys)
  96. def full_update(self, sleep_between_keys=0):
  97. """
  98. With the execution of this method, the complete source data which needs to be cached, will be read from the source instance
  99. and the resulting cache will be stored to the given file.
  100. :param sleep_between_keys: Time to sleep between each source data generation
  101. :type sleep_between_keys: float, int
  102. .. hint:: Use this method, if you initiallised the class with `store_on_get=False`
  103. """
  104. self._load_source(sleep_between_keys=sleep_between_keys)
  105. self._save_cache()
  106. def get(self, key, default=None):
  107. """
  108. Method to get the cached property. If the key does not exists in the cache or `source_instance`, `default` will be returned.
  109. :param key: key for value to get.
  110. :param default: value to be returned, if key does not exists.
  111. :returns: value for a given key or default value.
  112. """
  113. if key in self._source_instance.keys() and key not in self._source_get_keys:
  114. if self._cached_props is None:
  115. self._init_cache()
  116. if self._max_age is None:
  117. cache_old = False
  118. else:
  119. cache_old = time.time() - self._cached_props[self.AGE_TAG].get(self._key_filter(key), 0) > self._max_age
  120. if cache_old:
  121. logger.debug("The cached value is old, cached value will be ignored")
  122. if self._key_filter(key) not in self._cached_props[self.DATA_TAG] or cache_old:
  123. logger.debug("Loading property for key='%s' from source instance", key)
  124. val = self._source_instance.get(key, None)
  125. if self._store_on_get:
  126. tm = int(time.time())
  127. logger.debug("Adding key=%s, value=%s with timestamp=%d to chache", key, val, tm)
  128. self._cached_props[self.DATA_TAG][self._key_filter(key)] = val
  129. self._cached_props[self.AGE_TAG][self._key_filter(key)] = tm
  130. self._save_cache()
  131. else:
  132. return val
  133. else:
  134. logger.debug("Providing property for '%s' from cache", key)
  135. cached_data = self._cached_props[self.DATA_TAG].get(self._key_filter(key), default)
  136. if cached_data is None and self._return_source_on_none:
  137. return self._source_instance.get(key, default)
  138. return cached_data
  139. else:
  140. if key not in self._source_instance.keys():
  141. logger.debug("Key '%s' is not in cached_keys. Uncached data will be returned.", key)
  142. else:
  143. logger.debug("Key '%s' is excluded by .add_source_get_keys(). Uncached data will be returned.", key)
  144. return self._source_instance.get(key, default)
  145. def _data_version(self):
  146. if self._cached_props is None:
  147. return None
  148. else:
  149. return self._cached_props.get(self.DATA_VERSION_TAG, None)
  150. def _storage_version(self):
  151. if self._cached_props is None:
  152. return None
  153. else:
  154. return self._cached_props.get(self.STORAGE_VERSION_TAG, None)
  155. def _init_cache(self):
  156. load_cache = self._load_cache()
  157. uid = self._source_instance.uid() != self._uid()
  158. try:
  159. data_version = self._source_instance.data_version() > self._data_version()
  160. except TypeError:
  161. data_version = True
  162. try:
  163. storage_version = self._storage_version() != self.STORAGE_VERSION
  164. except TypeError:
  165. storage_version = True
  166. #
  167. if not load_cache or uid or data_version or storage_version:
  168. if load_cache:
  169. if self._uid() is not None and uid:
  170. logger.debug("Source uid changed, ignoring previous cache data")
  171. if self._data_version() is not None and data_version:
  172. logger.debug("Data version increased, ignoring previous cache data")
  173. if storage_version:
  174. logger.debug("Storage version changed, ignoring previous cache data")
  175. self._cached_props = {self.AGE_TAG: {}, self.DATA_TAG: {}}
  176. if self._load_all_on_init:
  177. self._load_source()
  178. self._cached_props[self.UID_TAG] = self._source_instance.uid()
  179. self._cached_props[self.DATA_VERSION_TAG] = self._source_instance.data_version()
  180. self._cached_props[self.STORAGE_VERSION_TAG] = self.STORAGE_VERSION
  181. def _load_only(self):
  182. with open(self._cache_filename, 'rb') as fh:
  183. self._cached_props = pickle.load(fh)
  184. logger.debug('Loading properties from cache (%s)', self._cache_filename)
  185. def _load_cache(self):
  186. if os.path.exists(self._cache_filename):
  187. self._load_only()
  188. return True
  189. else:
  190. logger.debug('Cache file does not exists (yet).')
  191. return False
  192. def _key_filter(self, key):
  193. return key
  194. def _load_source(self, sleep_between_keys=0):
  195. if self._cached_props is None:
  196. self._init_cache()
  197. logger.debug('Loading all data from source - %s', repr(self._source_instance.keys()))
  198. for key in self._source_instance.keys():
  199. if key not in self._source_get_keys:
  200. self._cached_props[self.DATA_TAG][self._key_filter(key)] = self._source_instance.get(key)
  201. self._cached_props[self.AGE_TAG][self._key_filter(key)] = int(time.time())
  202. time.sleep(sleep_between_keys)
  203. def _save_only(self):
  204. with open(self._cache_filename, 'wb') as fh:
  205. pickle.dump(self._cached_props, fh)
  206. logger.debug('cache-file stored (%s)', self._cache_filename)
  207. def _save_cache(self):
  208. self._save_only()
  209. if self._callback_on_data_storage is not None:
  210. self._callback_on_data_storage(self)
  211. def _uid(self):
  212. if self._cached_props is None:
  213. return None
  214. else:
  215. return self._cached_props.get(self.UID_TAG, None)
  216. class property_cache_json(property_cache_pickle):
  217. """
  218. See also parent :py:class:`property_cache_pickle` for detailed information.
  219. .. important::
  220. * This class uses json. You should **only** use keys of type string!
  221. * Unicode types are transfered to strings
  222. See limitations of json.
  223. **Example:**
  224. .. literalinclude:: caching/_examples_/property_cache_json.py
  225. Will result on the first execution to the following output (with a long execution time):
  226. .. literalinclude:: caching/_examples_/property_cache_json_1.log
  227. With every following execution the time cosumption my by much smaller:
  228. .. literalinclude:: caching/_examples_/property_cache_json_2.log
  229. """
  230. def _load_only(self):
  231. with open(self._cache_filename, 'r') as fh:
  232. self._cached_props = json.load(fh)
  233. logger.debug('Loading properties from cache (%s)', self._cache_filename)
  234. def _save_only(self):
  235. with open(self._cache_filename, 'w') as fh:
  236. json.dump(self._cached_props, fh, sort_keys=True, indent=4)
  237. logger.debug('cache-file stored (%s)', self._cache_filename)