123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- #
- """
- caching (Caching Module)
- ========================
-
- **Author:**
-
- * Dirk Alders <sudo-dirk@mount-mockery.de>
-
- **Description:**
-
- This Module supports functions and classes for caching e.g. properties of other instances.
-
- **Submodules:**
-
- * :class:`caching.property_cache_json`
- * :class:`caching.property_cache_pickle`
-
- **Unittest:**
-
- See also the :download:`unittest <caching/_testresults_/unittest.pdf>` documentation.
- """
- __DEPENDENCIES__ = []
-
- import json
- import logging
- import os
- import pickle
- import time
-
- try:
- from config import APP_NAME as ROOT_LOGGER_NAME
- except ImportError:
- ROOT_LOGGER_NAME = 'root'
- logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
-
- __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.
- For more Information read the documentation.""" % __name__.replace('_', '\_')
- """The Module Description"""
- __INTERPRETER__ = (3, )
- """The Tested Interpreter-Versions"""
-
-
- class property_cache_pickle(object):
- """
- Class to cache properties, which take longer on initialising than reading a file in pickle format.
-
- :param source_instance: The source instance holding the data
- :type source_instance: instance
- :param cache_filename: File name, where the properties are stored as cache
- :type cache_filename: str
- :param load_all_on_init: Optionally init behaviour control parameter. True will load all available properties from source on init, False not.
- :param max_age: The maximum age of the cache object, after that time the source will be used for getting information.
- :type max_age: int
- :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
-
- .. note:: source_instance needs to have at least the following methods: uid(), keys(), data_version(), get()
-
- * uid(): returns the unique id of the source.
- * keys(): returns a list of all available keys.
- * 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).
- * get(key, default): returns the property for a key. If key does not exists, default will be returned.
-
- Reasons for updating the complete data set:
-
- * UID of source_instance has changed (in comparison to the cached value).
- * data_version is increased
-
- **Example:**
-
- .. literalinclude:: caching/_examples_/property_cache_pickle.py
-
- Will result on the first execution to the following output (with a long execution time):
-
- .. literalinclude:: caching/_examples_/property_cache_pickle_1.log
-
- With every following execution (slow for getting "two" which is not cached - see implementation):
-
- .. literalinclude:: caching/_examples_/property_cache_pickle_2.log
- """
- LOG_PREFIX = 'PickCache:'
- DATA_VERSION_TAG = '_property_cache_data_version_'
- STORAGE_VERSION_TAG = '_storage_version_'
- UID_TAG = '_property_cache_uid_'
- DATA_TAG = '_data_'
- AGE_TAG = '_age_'
- #
- STORAGE_VERSION = 1
-
- def __init__(self, source_instance, cache_filename, load_all_on_init=False, callback_on_data_storage=None, max_age=None, store_on_get=True):
- self._source_instance = source_instance
- self._cache_filename = cache_filename
- self._load_all_on_init = load_all_on_init
- self._callback_on_data_storage = callback_on_data_storage
- self._max_age = max_age
- self._store_on_get = store_on_get
- #
- self._source_get_keys = []
- self._cached_props = None
-
- def get(self, key, default=None):
- """
- 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).
-
- :param key: key for value to get.
- :param default: value to be returned, if key does not exists.
- :returns: value for a given key or default value.
- """
- if key in self.keys() and key not in self._source_get_keys:
- if self._cached_props is None:
- self._init_cache()
- if self._max_age is None:
- cache_old = False
- else:
- cache_old = time.time() - self._cached_props[self.AGE_TAG].get(self._key_filter(key), 0) > self._max_age
- if cache_old:
- logger.debug("The cached value is old, cached value will be ignored")
- if self._key_filter(key) not in self._cached_props[self.DATA_TAG] or cache_old:
- val = self._source_instance.get(key, None)
- logger.debug("%s Loading property for '%s' from source instance (%s)", self.LOG_PREFIX, key, repr(val))
- if self._store_on_get:
- tm = int(time.time())
- logger.debug("Storing value=%s with timestamp=%d to chache", val, tm)
- self._cached_props[self.DATA_TAG][self._key_filter(key)] = val
- self._cached_props[self.AGE_TAG][self._key_filter(key)] = tm
- self._save_cache()
- else:
- return val
- else:
- logger.debug("%s Providing property for '%s' from cache (%s)", self.LOG_PREFIX,
- key, repr(self._cached_props[self.DATA_TAG].get(self._key_filter(key), default)))
- return self._cached_props[self.DATA_TAG].get(self._key_filter(key), default)
- else:
- logger.info("%s Key '%s' is not in cached_keys. Uncached data will be returned.", self.LOG_PREFIX, key)
- return self._source_instance.get(key, default)
-
- def full_update(self):
- self._load_source()
- self._save_cache()
-
- def keys(self):
- """
- Method to get the available keys (from :data:`source_instance`).
- """
- return self._source_instance.keys()
-
- def _data_version(self):
- if self._cached_props is None:
- return None
- else:
- return self._cached_props.get(self.DATA_VERSION_TAG, None)
-
- def _storage_version(self):
- if self._cached_props is None:
- return None
- else:
- return self._cached_props.get(self.STORAGE_VERSION_TAG, None)
-
- def _init_cache(self):
- load_cache = self._load_cache()
- uid = self._source_instance.uid() != self._uid()
- try:
- data_version = self._source_instance.data_version() > self._data_version()
- except TypeError:
- data_version = True
- try:
- storage_version = self._storage_version() != self.STORAGE_VERSION
- except TypeError:
- storage_version = True
- #
- if not load_cache or uid or data_version or storage_version:
- if load_cache:
- if self._uid() is not None and uid:
- logger.debug("%s Source uid changed, ignoring previous cache data", self.LOG_PREFIX)
- if self._data_version() is not None and data_version:
- logger.debug("%s Data version increased, ignoring previous cache data", self.LOG_PREFIX)
- if storage_version:
- logger.debug("%s Storage version changed, ignoring previous cache data", self.LOG_PREFIX)
- self._cached_props = {self.AGE_TAG: {}, self.DATA_TAG: {}}
- if self._load_all_on_init:
- self._load_source()
- self._cached_props[self.UID_TAG] = self._source_instance.uid()
- self._cached_props[self.DATA_VERSION_TAG] = self._source_instance.data_version()
- self._cached_props[self.STORAGE_VERSION_TAG] = self.STORAGE_VERSION
- self._save_cache()
-
- def _load_cache(self):
- if os.path.exists(self._cache_filename):
- with open(self._cache_filename, 'rb') as fh:
- self._cached_props = pickle.load(fh)
- logger.info('%s Loading properties from cache (%s)', self.LOG_PREFIX, self._cache_filename)
- return True
- else:
- logger.debug('%s Cache file does not exists (yet).', self.LOG_PREFIX)
- return False
-
- def _key_filter(self, key):
- return key
-
- def _load_source(self):
- if self._cached_props is None:
- self._init_cache()
- logger.debug('%s Loading all data from source - %s', self.LOG_PREFIX, repr(self.keys()))
- for key in self.keys():
- if key not in self._source_get_keys:
- self._cached_props[self.DATA_TAG][self._key_filter(key)] = self._source_instance.get(key)
- self._cached_props[self.AGE_TAG][self._key_filter(key)] = int(time.time())
-
- def _save_cache(self):
- with open(self._cache_filename, 'wb') as fh:
- pickle.dump(self._cached_props, fh)
- logger.info('%s cache-file stored (%s)', self.LOG_PREFIX, self._cache_filename)
- if self._callback_on_data_storage is not None:
- self._callback_on_data_storage()
-
- def _uid(self):
- if self._cached_props is None:
- return None
- else:
- return self._cached_props.get(self.UID_TAG, None)
-
- def add_source_get_keys(self, keys):
- if type(keys) in [list, tuple]:
- self._source_get_keys.extend(keys)
- else:
- self._source_get_keys.append(keys)
-
- def __getattribute__(self, name):
- try:
- return super().__getattribute__(name)
- except AttributeError:
- return getattr(self._source_instance, name)
-
-
- class property_cache_json(property_cache_pickle):
- """
- Class to cache properties, which take longer on initialising than reading a file in json format. See also parent :py:class:`property_cache_pickle`
-
- :param source_instance: The source instance holding the data
- :type source_instance: instance
- :param cache_filename: File name, where the properties are stored as cache
- :type cache_filename: str
- :param load_all_on_init: Optionally init behaviour control parameter. True will load all available properties from source on init, False not.
- :param max_age: The maximum age of the cache object, after that time the source will be used for getting information.
- :type max_age: int
- :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
-
- .. warning::
- * This class uses json. You should **only** use keys of type string!
- * Unicode types are transfered to strings
-
- .. note:: source_instance needs to have at least the following methods: uid(), keys(), data_version(), get()
-
- * uid(): returns the unique id of the source.
- * keys(): returns a list of all available keys.
- * 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).
- * get(key, default): returns the property for a key. If key does not exists, default will be returned.
-
- Reasons for updating the complete data set:
-
- * UID of source_instance has changed (in comparison to the cached value).
- * data_version is increased
-
- **Example:**
-
- .. literalinclude:: caching/_examples_/property_cache_json.py
-
- Will result on the first execution to the following output (with a long execution time):
-
- .. literalinclude:: caching/_examples_/property_cache_json_1.log
-
- With every following execution (slow for getting "two" which is not cached - see implementation):
-
- .. literalinclude:: caching/_examples_/property_cache_json_2.log
- """
- LOG_PREFIX = 'JsonCache:'
-
- def _load_cache(self):
- if os.path.exists(self._cache_filename):
- with open(self._cache_filename, 'r') as fh:
- self._cached_props = json.load(fh)
- logger.info('%s Loading properties from cache (%s)', self.LOG_PREFIX, self._cache_filename)
- return True
- else:
- logger.debug('%s Cache file does not exists (yet).', self.LOG_PREFIX)
- return False
-
- def _save_cache(self):
- with open(self._cache_filename, 'w') as fh:
- json.dump(self._cached_props, fh, sort_keys=True, indent=4)
- logger.info('%s cache-file stored (%s)', self.LOG_PREFIX, self._cache_filename)
- if self._callback_on_data_storage is not None:
- self._callback_on_data_storage()
|