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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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 hashlib
  19. import hmac
  20. import json
  21. import logging
  22. import os
  23. import pickle
  24. import sys
  25. try:
  26. from config import APP_NAME as ROOT_LOGGER_NAME
  27. except ImportError:
  28. ROOT_LOGGER_NAME = 'root'
  29. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  30. __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.
  31. For more Information read the documentation.""" % __name__.replace('_', '\_')
  32. """The Module Description"""
  33. __INTERPRETER__ = (2, 3)
  34. """The Tested Interpreter-Versions"""
  35. class property_cache_pickle(object):
  36. """
  37. Class to cache properties, which take longer on initialising than reading a file in pickle format.
  38. :param source_instance: The source instance holding the data
  39. :type source_instance: instance
  40. :param cache_filename: File name, where the properties are stored as cache
  41. :type cache_filename: str
  42. :param load_all_on_init: Optionally init behaviour control parameter. True will load all available properties from source on init, False not.
  43. :raises: ?
  44. .. note:: This module uses logging. So logging should be initialised at least by executing logging.basicConfig(...)
  45. .. note:: source_instance needs to have at least the following methods: uid(), keys(), data_version(), get()
  46. * uid(): returns the unique id of the source.
  47. * keys(): returns a list of all available keys.
  48. * 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).
  49. * get(key, default): returns the property for a key. If key does not exists, default will be returned.
  50. Reasons for updating the complete data set:
  51. * UID of source_instance has changed (in comparison to the cached value).
  52. * data_version is increased
  53. **Example:**
  54. .. literalinclude:: ../../caching/_examples_/property_cache_pickle.py
  55. Will result on the first execution to the following output (with a long execution time):
  56. .. literalinclude:: ../../caching/_examples_/property_cache_pickle_1.log
  57. With every following execution (slow for getting "two" which is not cached - see implementation):
  58. .. literalinclude:: ../../caching/_examples_/property_cache_pickle_2.log
  59. """
  60. LOG_PREFIX = 'PickCache:'
  61. DATA_VERSION_TAG = '_property_cache_data_version_'
  62. UID_TAG = '_property_cache_uid_'
  63. def __init__(self, source_instance, cache_filename, load_all_on_init=False, callback_on_data_storage=None):
  64. self._source_instance = source_instance
  65. self._cache_filename = cache_filename
  66. self._load_all_on_init = load_all_on_init
  67. self._callback_on_data_storage = callback_on_data_storage
  68. self._cached_props = None
  69. def get(self, key, default=None):
  70. """
  71. 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).
  72. :param key: key for value to get.
  73. :param default: value to be returned, if key does not exists.
  74. :returns: value for a given key or default value.
  75. """
  76. if key in self.keys():
  77. if self._cached_props is None:
  78. self._init_cache()
  79. if self._key_filter(key) not in self._cached_props:
  80. val = self._source_instance.get(key, None)
  81. logger.debug("%s Loading property for '%s' from source instance (%s)", self.LOG_PREFIX, key, repr(val))
  82. self._cached_props[self._key_filter(key)] = val
  83. self._save_cache()
  84. else:
  85. logger.debug("%s Providing property for '%s' from cache (%s)", self.LOG_PREFIX, key, repr(self._cached_props.get(self._key_filter(key), default)))
  86. return self._cached_props.get(self._key_filter(key), default)
  87. else:
  88. logger.info("%s Key '%s' is not in cached_keys. Uncached data will be returned.", self.LOG_PREFIX, key)
  89. return self._source_instance.get(key, default)
  90. def keys(self):
  91. return self._source_instance.keys()
  92. def _data_version(self):
  93. if self._cached_props is None:
  94. return None
  95. else:
  96. return self._cached_props.get(self.DATA_VERSION_TAG, None)
  97. def _init_cache(self):
  98. if not self._load_cache() or self._source_instance.uid() != self._uid() or self._source_instance.data_version() > self._data_version():
  99. if self._uid() is not None and self._source_instance.uid() != self._uid():
  100. logger.debug("%s Source uid changed, ignoring previous cache data", self.LOG_PREFIX)
  101. if self._data_version() is not None and self._source_instance.data_version() > self._data_version():
  102. logger.debug("%s Data version increased, ignoring previous cache data", self.LOG_PREFIX)
  103. self._cached_props = dict()
  104. if self._load_all_on_init:
  105. self._load_source()
  106. self._cached_props[self.UID_TAG] = self._source_instance.uid()
  107. self._cached_props[self.DATA_VERSION_TAG] = self._source_instance.data_version()
  108. self._save_cache()
  109. def _load_cache(self):
  110. if os.path.exists(self._cache_filename):
  111. with open(self._cache_filename, 'rb') as fh:
  112. self._cached_props = pickle.load(fh)
  113. logger.info('%s Loading properties from cache (%s)', self.LOG_PREFIX, self._cache_filename)
  114. return True
  115. else:
  116. logger.debug('%s Cache file does not exists (yet).', self.LOG_PREFIX)
  117. return False
  118. def _key_filter(self, key):
  119. if sys.version_info >= (3, 0):
  120. tps = [str]
  121. else:
  122. tps = [str, unicode]
  123. if type(key) in tps:
  124. if key.endswith(self.DATA_VERSION_TAG) or key.endswith(self.UID_TAG):
  125. return '_' + key
  126. return key
  127. def _load_source(self):
  128. logger.debug('%s Loading all data from source - %s', self.LOG_PREFIX, repr(self._source_instance.keys()))
  129. for key in self._source_instance.keys():
  130. val = self._source_instance.get(key)
  131. self._cached_props[self._key_filter(key)] = val
  132. def _save_cache(self):
  133. with open(self._cache_filename, 'wb') as fh:
  134. pickle.dump(self._cached_props, fh)
  135. logger.info('%s cache-file stored (%s)', self.LOG_PREFIX, self._cache_filename)
  136. if self._callback_on_data_storage is not None:
  137. self._callback_on_data_storage()
  138. def _uid(self):
  139. if self._cached_props is None:
  140. return None
  141. else:
  142. return self._cached_props.get(self.UID_TAG, None)
  143. class property_cache_json(property_cache_pickle):
  144. """
  145. Class to cache properties, which take longer on initialising than reading a file in json format. See also parent :py:class:`property_cache_pickle`
  146. :param source_instance: The source instance holding the data
  147. :type source_instance: instance
  148. :param cache_filename: File name, where the properties are stored as cache
  149. :type cache_filename: str
  150. :param load_all_on_init: Optionally init behaviour control parameter. True will load all available properties from source on init, False not.
  151. :raises: ?
  152. .. warning::
  153. * This class uses json. You should **only** use keys of type string!
  154. * Unicode types are transfered to strings
  155. .. note:: This module uses logging. So logging should be initialised at least by executing logging.basicConfig(...)
  156. .. note:: source_instance needs to have at least the following methods: uid(), keys(), data_version(), get()
  157. * uid(): returns the unique id of the source.
  158. * keys(): returns a list of all available keys.
  159. * 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).
  160. * get(key, default): returns the property for a key. If key does not exists, default will be returned.
  161. Reasons for updating the complete data set:
  162. * UID of source_instance has changed (in comparison to the cached value).
  163. * data_version is increased
  164. **Example:**
  165. .. literalinclude:: ../../caching/_examples_/property_cache_json.py
  166. Will result on the first execution to the following output (with a long execution time):
  167. .. literalinclude:: ../../caching/_examples_/property_cache_json_1.log
  168. With every following execution (slow for getting "two" which is not cached - see implementation):
  169. .. literalinclude:: ../../caching/_examples_/property_cache_json_2.log
  170. """
  171. LOG_PREFIX = 'JsonCache:'
  172. def _load_cache(self):
  173. if os.path.exists(self._cache_filename):
  174. with open(self._cache_filename, 'r') as fh:
  175. self._cached_props = json.load(fh)
  176. logger.info('%s Loading properties from cache (%s)', self.LOG_PREFIX, self._cache_filename)
  177. return True
  178. else:
  179. logger.debug('%s Cache file does not exists (yet).', self.LOG_PREFIX)
  180. return False
  181. def _save_cache(self):
  182. with open(self._cache_filename, 'w') as fh:
  183. json.dump(self._cached_props, fh, sort_keys=True, indent=4)
  184. logger.info('%s cache-file stored (%s)', self.LOG_PREFIX, self._cache_filename)
  185. if self._callback_on_data_storage is not None:
  186. self._callback_on_data_storage()