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.

__init__.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. Class to cache properties, which take longer on initialising than reading a file in pickle format.
  36. :param source_instance: The source instance holding the data
  37. :type source_instance: instance
  38. :param cache_filename: File name, where the properties are stored as cache
  39. :type cache_filename: str
  40. :param load_all_on_init: Optionally init behaviour control parameter. True will load all available properties from source on init, False not.
  41. :param max_age: The maximum age of the cache object, after that time the source will be used for getting information.
  42. :type max_age: int
  43. :param store_on_get: Parameter to enable / disable cache storage on get. If you disble data storage, you need to update and store the cache manually
  44. .. note:: source_instance needs to have at least the following methods: uid(), keys(), data_version(), get()
  45. * uid(): returns the unique id of the source.
  46. * keys(): returns a list of all available keys.
  47. * 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).
  48. * get(key, default): returns the property for a key. If key does not exists, default will be returned.
  49. Reasons for updating the complete data set:
  50. * UID of source_instance has changed (in comparison to the cached value).
  51. * data_version is increased
  52. **Example:**
  53. .. literalinclude:: caching/_examples_/property_cache_pickle.py
  54. Will result on the first execution to the following output (with a long execution time):
  55. .. literalinclude:: caching/_examples_/property_cache_pickle_1.log
  56. With every following execution (slow for getting "two" which is not cached - see implementation):
  57. .. literalinclude:: caching/_examples_/property_cache_pickle_2.log
  58. """
  59. LOG_PREFIX = 'PickCache:'
  60. DATA_VERSION_TAG = '_property_cache_data_version_'
  61. STORAGE_VERSION_TAG = '_storage_version_'
  62. UID_TAG = '_property_cache_uid_'
  63. DATA_TAG = '_data_'
  64. AGE_TAG = '_age_'
  65. #
  66. STORAGE_VERSION = 1
  67. def __init__(self, source_instance, cache_filename, load_all_on_init=False, callback_on_data_storage=None, max_age=None, store_on_get=True):
  68. self._source_instance = source_instance
  69. self._cache_filename = cache_filename
  70. self._load_all_on_init = load_all_on_init
  71. self._callback_on_data_storage = callback_on_data_storage
  72. self._max_age = max_age
  73. self._store_on_get = store_on_get
  74. #
  75. self._source_get_keys = []
  76. self._cached_props = None
  77. def get(self, key, default=None):
  78. """
  79. Method to get the cached property. If key does not exists in cache, the property will be loaded from source_instance and stored in cache (file).
  80. :param key: key for value to get.
  81. :param default: value to be returned, if key does not exists.
  82. :returns: value for a given key or default value.
  83. """
  84. if key in self.keys() and key not in self._source_get_keys:
  85. if self._cached_props is None:
  86. self._init_cache()
  87. if self._max_age is None:
  88. cache_old = False
  89. else:
  90. cache_old = time.time() - self._cached_props[self.AGE_TAG].get(self._key_filter(key), 0) > self._max_age
  91. if cache_old:
  92. logger.debug("The cached value is old, cached value will be ignored")
  93. if self._key_filter(key) not in self._cached_props[self.DATA_TAG] or cache_old:
  94. val = self._source_instance.get(key, None)
  95. logger.debug("%s Loading property for '%s' from source instance (%s)", self.LOG_PREFIX, key, repr(val))
  96. if self._store_on_get:
  97. tm = int(time.time())
  98. logger.debug("Storing value=%s with timestamp=%d to chache", val, tm)
  99. self._cached_props[self.DATA_TAG][self._key_filter(key)] = val
  100. self._cached_props[self.AGE_TAG][self._key_filter(key)] = tm
  101. self._save_cache()
  102. else:
  103. return val
  104. else:
  105. logger.debug("%s Providing property for '%s' from cache (%s)", self.LOG_PREFIX,
  106. key, repr(self._cached_props[self.DATA_TAG].get(self._key_filter(key), default)))
  107. return self._cached_props[self.DATA_TAG].get(self._key_filter(key), default)
  108. else:
  109. logger.info("%s Key '%s' is not in cached_keys. Uncached data will be returned.", self.LOG_PREFIX, key)
  110. return self._source_instance.get(key, default)
  111. def full_update(self):
  112. self._load_source()
  113. self._save_cache()
  114. def keys(self):
  115. """
  116. Method to get the available keys (from :data:`source_instance`).
  117. """
  118. return self._source_instance.keys()
  119. def _data_version(self):
  120. if self._cached_props is None:
  121. return None
  122. else:
  123. return self._cached_props.get(self.DATA_VERSION_TAG, None)
  124. def _storage_version(self):
  125. if self._cached_props is None:
  126. return None
  127. else:
  128. return self._cached_props.get(self.STORAGE_VERSION_TAG, None)
  129. def _init_cache(self):
  130. load_cache = self._load_cache()
  131. uid = self._source_instance.uid() != self._uid()
  132. try:
  133. data_version = self._source_instance.data_version() > self._data_version()
  134. except TypeError:
  135. data_version = True
  136. try:
  137. storage_version = self._storage_version() != self.STORAGE_VERSION
  138. except TypeError:
  139. storage_version = True
  140. #
  141. if not load_cache or uid or data_version or storage_version:
  142. if load_cache:
  143. if self._uid() is not None and uid:
  144. logger.debug("%s Source uid changed, ignoring previous cache data", self.LOG_PREFIX)
  145. if self._data_version() is not None and data_version:
  146. logger.debug("%s Data version increased, ignoring previous cache data", self.LOG_PREFIX)
  147. if storage_version:
  148. logger.debug("%s Storage version changed, ignoring previous cache data", self.LOG_PREFIX)
  149. self._cached_props = {self.AGE_TAG: {}, self.DATA_TAG: {}}
  150. if self._load_all_on_init:
  151. self._load_source()
  152. self._cached_props[self.UID_TAG] = self._source_instance.uid()
  153. self._cached_props[self.DATA_VERSION_TAG] = self._source_instance.data_version()
  154. self._cached_props[self.STORAGE_VERSION_TAG] = self.STORAGE_VERSION
  155. self._save_cache()
  156. def _load_cache(self):
  157. if os.path.exists(self._cache_filename):
  158. with open(self._cache_filename, 'rb') as fh:
  159. self._cached_props = pickle.load(fh)
  160. logger.info('%s Loading properties from cache (%s)', self.LOG_PREFIX, self._cache_filename)
  161. return True
  162. else:
  163. logger.debug('%s Cache file does not exists (yet).', self.LOG_PREFIX)
  164. return False
  165. def _key_filter(self, key):
  166. return key
  167. def _load_source(self):
  168. if self._cached_props is None:
  169. self._init_cache()
  170. logger.debug('%s Loading all data from source - %s', self.LOG_PREFIX, repr(self.keys()))
  171. for key in self.keys():
  172. if key not in self._source_get_keys:
  173. self._cached_props[self.DATA_TAG][self._key_filter(key)] = self._source_instance.get(key)
  174. self._cached_props[self.AGE_TAG][self._key_filter(key)] = int(time.time())
  175. def _save_cache(self):
  176. with open(self._cache_filename, 'wb') as fh:
  177. pickle.dump(self._cached_props, fh)
  178. logger.info('%s cache-file stored (%s)', self.LOG_PREFIX, self._cache_filename)
  179. if self._callback_on_data_storage is not None:
  180. self._callback_on_data_storage()
  181. def _uid(self):
  182. if self._cached_props is None:
  183. return None
  184. else:
  185. return self._cached_props.get(self.UID_TAG, None)
  186. def add_source_get_keys(self, keys):
  187. if type(keys) in [list, tuple]:
  188. self._source_get_keys.extend(keys)
  189. else:
  190. self._source_get_keys.append(keys)
  191. def __getattribute__(self, name):
  192. try:
  193. return super().__getattribute__(name)
  194. except AttributeError:
  195. return getattr(self._source_instance, name)
  196. class property_cache_json(property_cache_pickle):
  197. """
  198. Class to cache properties, which take longer on initialising than reading a file in json format. See also parent :py:class:`property_cache_pickle`
  199. :param source_instance: The source instance holding the data
  200. :type source_instance: instance
  201. :param cache_filename: File name, where the properties are stored as cache
  202. :type cache_filename: str
  203. :param load_all_on_init: Optionally init behaviour control parameter. True will load all available properties from source on init, False not.
  204. :param max_age: The maximum age of the cache object, after that time the source will be used for getting information.
  205. :type max_age: int
  206. :param store_on_get: Parameter to enable / disable cache storage on get. If you disble data storage, you need to update and store the cache manually
  207. .. warning::
  208. * This class uses json. You should **only** use keys of type string!
  209. * Unicode types are transfered to strings
  210. .. note:: source_instance needs to have at least the following methods: uid(), keys(), data_version(), get()
  211. * uid(): returns the unique id of the source.
  212. * keys(): returns a list of all available keys.
  213. * 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).
  214. * get(key, default): returns the property for a key. If key does not exists, default will be returned.
  215. Reasons for updating the complete data set:
  216. * UID of source_instance has changed (in comparison to the cached value).
  217. * data_version is increased
  218. **Example:**
  219. .. literalinclude:: caching/_examples_/property_cache_json.py
  220. Will result on the first execution to the following output (with a long execution time):
  221. .. literalinclude:: caching/_examples_/property_cache_json_1.log
  222. With every following execution (slow for getting "two" which is not cached - see implementation):
  223. .. literalinclude:: caching/_examples_/property_cache_json_2.log
  224. """
  225. LOG_PREFIX = 'JsonCache:'
  226. def _load_cache(self):
  227. if os.path.exists(self._cache_filename):
  228. with open(self._cache_filename, 'r') as fh:
  229. self._cached_props = json.load(fh)
  230. logger.info('%s Loading properties from cache (%s)', self.LOG_PREFIX, self._cache_filename)
  231. return True
  232. else:
  233. logger.debug('%s Cache file does not exists (yet).', self.LOG_PREFIX)
  234. return False
  235. def _save_cache(self):
  236. with open(self._cache_filename, 'w') as fh:
  237. json.dump(self._cached_props, fh, sort_keys=True, indent=4)
  238. logger.info('%s cache-file stored (%s)', self.LOG_PREFIX, self._cache_filename)
  239. if self._callback_on_data_storage is not None:
  240. self._callback_on_data_storage()