# -*- coding: utf-8 -*-
"""
This module provides base handler functions, and basic functionality. Will
successfully operate as a handler, but if used as one will only store data in-memory.
"""
from __future__ import absolute_import
import cacheblob
from .. import constants as C
from ..item import CacheItem
[docs]def is_number(value):
"""Return if value is numeric, based on attempting to coerce to float.
:param value: The value in question.
:returns: True if the float(value) does not throw an exception, otherwise False.
"""
try:
float(value)
return True
except ValueError:
return False
[docs]class Handler(object):
"""This module represents a handler for `cacheblob.item` objects. Handlers offer
simple fetch() and store() capabilities, and other utilities. Items included are
expected to be unique by index.
"""
def __init__(self, opts=None):
self._overwrite = True
self._index_memoized = {}
self._memoize = True
self._memoize_max_length = C.MEMOIZE_MAX_LENGTH
self._memoize_max_entries = C.MEMOIZE_MAX_ENTRIES
self._may_purge_if_expired = True
if opts is not None:
if 'overwrite' in opts:
self.overwrite = opts['overwrite']
if 'memoize' in opts:
self.memoize = opts['memoize']
# This is the maximum value length that can be held in a memoized item.
if 'memoize_max_length' in opts:
if not self.memoize or ('memoize' in opts and not opts['memoize']):
raise ValueError("memoize_max_length ony applicable if memoize is true")
self.memoize_max_length = opts['memoize_max_length']
# This is the maximum number of items that will be memoized.
if 'memoize_max_entries' in opts:
if not self.memoize or ('memoize' in opts and not opts['memoize']):
raise ValueError("memoize_max_entries ony applicable if memoize is true")
self.memoize_max_entries = opts['memoize_max_entries']
# This is whether or not the handler can purge expired items from the
# underlying storage. This is not a promise that it necessarily will.
if 'may_purge_if_expired' in opts:
self.may_purge_if_expired = opts['may_purge_if_expired']
def __iter__(self):
return self.fetch_all()
def __contains__(self, index):
for item in self.fetch_all():
if item.index == index:
return True
return False
@property
def overwrite(self):
"""Get the overwrite status.
:returns: Whether or not the object will overwrite entries with the same index.
"""
return self._overwrite
@overwrite.setter
def overwrite(self, enable_overwrite):
"""Define the overwrite status. This is whether or not an existing item will be
overwriting if a second item with the same index is stored afterwards.
:param enable_overwrite: Whether or not items should be overwritten by newer ones.
"""
self._overwrite = bool(enable_overwrite)
@property
def memoize(self):
"""Get the memoization status.
:returns: Whether or not in-memory memoization is enabled.
"""
return self._memoize
@memoize.setter
def memoize(self, enable_memoize):
"""Enable or disable in-memory memoization. This is designed to make accessing
quicker.
:param enable_memoize: Whether or not items should be memoized.
"""
self._memoize = bool(enable_memoize)
@property
def may_purge_if_expired(self):
"""Returns whether or not expired items may be purged from the underlying
persistent data store. Note that this does not guarantee that they will be
purged, but instead allows them to be purged.
:returns: Whether items may be purged if they are expired.
"""
return self._may_purge_if_expired
@may_purge_if_expired.setter
def may_purge_if_expired(self, allow_purge):
"""Enable or disable purging of the underlying data storage upon item expiry.
This is not a promise that it necessarily will, be if enabled will allow the
handler to clean purged data as it pleases. Accessing an expired item should not
be affected by whether this is set or not.
:param allow_purge: Whether or not purging of expired items is allowed.
"""
self._may_purge_if_expired = bool(allow_purge)
@property
def memoize_max_length(self):
"""Get the max value length allowed for memoized items.
:returns: The maximum value length allowed for items memoized.
"""
return self._memoize_max_length
@memoize_max_length.setter
def memoize_max_length(self, max_length):
"""Set the maximum value length of items that can be memoized. Note that this is
not the maximum number of items that can be memoized, but instead the max size of
them individually.
:param max_length: The maximum value length allowed.
"""
if is_number(max_length):
self._memoize_max_length = max_length
else:
raise TypeError("memoize_max_length must be numeric")
@property
def memoize_max_entries(self):
"""Get the max number of memoized items at a given time.
:returns: The maximum number of items that will be memoized.
"""
return self._memoize_max_entries
@memoize_max_entries.setter
def memoize_max_entries(self, max_entries):
"""Set the maximum number of of items that can be memoized.
:param max_entries: The maximum number of items that can be memoized.
"""
if is_number(max_entries):
self._memoize_max_entries = max_entries
else:
raise TypeError("memoize_max_entries must be numeric")
def _get_memoized(self, index):
"""Get the value of a memoized item. This is an internal utility that is not
designed to be called by users.
:param index: The index of the item to be retrieved.
:returns: The item, and whether an item was found or not. If the item is expired
then it will not be returned, but the second value will be True.
"""
if self.memoize and index in self._index_memoized:
item = self._index_memoized[index]
# Memoized items are easy to purge, so we do so if applicable.
if self.may_purge_if_expired and item.is_expired:
self.purge(index)
return None, True
return item, True
return None, False
def _memoize_if_applicable(self, item):
"""Memoize an item, if enabled. This is an internal utility that is not designed
to be called by users.
:param item: The item to be stored.
:returns: True if an item was stored, otherwise False.
"""
if self.memoize and (not item.length or item.length <= self.memoize_max_length):
if item.index not in self._index_memoized or self.overwrite:
# Remove an arbitrary existing item.
if (self.memoize_max_entries is not None and
len(self._index_memoized) >= self.memoize_max_entries):
self._index_memoized.popitem()
self._index_memoized[item.index] = item
return True
return False
def _delete_memoized(self, index):
"""Delete a memoized item. This is an internal utility that is not designed to be
called by users. Note that this does not remove an item from the persistent
storage mechanism.
:param index: The index of the item to be deleted.
:returns: True if an item was removed, otherwise False.
"""
if index in self._index_memoized:
del self._index_memoized[index]
return True
return False
def _store_item(self, item):
"""Store an item in the handler.
:param item: The item to be stored.
:returns: True if the item was successfully stored, False otherwise.
"""
return self._memoize_if_applicable(item)
[docs] def get_indexes(self):
"""Get all of the indexes stored in the handler.
:returns: The list of indexes.
"""
return list(self._index_memoized.keys())
[docs] def purge(self, index):
"""Purge an item from the handler. The persistent value will be removed.
:param index: The index of the item to be purged.
:returns: Whether or not an item was removed.
"""
return self._delete_memoized(index)
[docs] def purge_if_expired(self, index):
"""If an item is expired, purge it.
:param index: The index of the item to be purged.
:returns: Whether or not an item was removed.
"""
(item, found) = self.fetch_with_status(index)
if (item and item.is_expired) or (found and not item):
self.purge(index)
return True
return False
[docs] def purge_any_expired(self):
"""Purge any items that can be purged, based on their expiry.
:returns: Count of items purged.
"""
purged_count = 0
for index in self.get_indexes():
if self.purge_if_expired(index):
purged_count += 1
return purged_count
[docs] def store(self, *args, **kwargs):
"""Store an entry in the handler. Parameters can be either a `CacheItem`
or parameters to create one
:param args|item: a `CacheItem` object, or arguments to create one.
:param kwargs: Keyword arguments to create a `CacheItem` object.
"""
if len(args) and isinstance(args[0], cacheblob.item.CacheItem):
return self._store_item(args[0])
else:
item = CacheItem(*args, **kwargs)
return self._store_item(item)
[docs] def fetch(self, index):
"""Fetch an item from the handler.
:param index: Index of the item to fetch. Indexes are expected to be unique at
any given time.
:returns: The item, or None if no unexpired items are found.
"""
(item, _) = self._get_memoized(index)
return item
[docs] def fetch_with_status(self, index):
"""Fetch an item from the handler. This differs from fetch() because it will also
return whether or not an item was found, which differs in the case of expired
items.
:param index: Index of the item to fetch. Indexes are expected to be unique at
any given time.
:returns: The item, or None if no unexpired items are found, and whether an item
was found.
"""
return self._get_memoized(index)
[docs] def fetch_all(self):
"""Fetch all of the items and return an iterator. Note that this function does
not guarantee that the items will not be modified between this function returning
and the user accessing the values.
:returns: An iterator that yields items.
"""
all_indexes = list(self._index_memoized.keys())
for index in all_indexes:
item = self.fetch(index)
if item:
yield item
[docs] def fetch_all_with_status(self):
"""Fetch all of the items and return an iterator. Note that this function does
not guarantee that the items will not be modified between this function returning
and the user accessing the values. This differs from fetch_all() because it will
also return whether or not an item was found, which differs in the case of
expired items.
:returns: An iterator that yields pairs of items and their status.
"""
all_indexes = list(self._index_memoized.keys())
for index in all_indexes:
yield self.fetch_with_status(index)
[docs] def close(self):
"""Close any associated databases. For this default handler, this does nothing.
"""
pass