Source code for birding.shelf

"""Track terms using a simple dict-like interface."""

import abc
import collections
import time

import elasticsearch
from repoze.lru import LRUCache

from .config import import_name


UNSET = object()


[docs]def shelf_from_config(config, **default_init): """Get a `Shelf` instance dynamically based on config. `config` is a dictionary containing ``shelf_*`` keys as defined in :mod:`birding.config`. """ shelf_cls = import_name(config['shelf_class'], default_ns='birding.shelf') init = {} init.update(default_init) init.update(config['shelf_init']) shelf = shelf_cls(**init) if hasattr(shelf, 'set_expiration') and 'shelf_expiration' in config: shelf.set_expiration(config['shelf_expiration']) return shelf
[docs]class Shelf(collections.MutableMapping): """Abstract base class for a shelf to track -- but not iterate -- values. Provides a dict-interface. """ __metaclass__ = abc.ABCMeta @abc.abstractmethod
[docs] def getitem(self, key): """Get an item's value from the shelf or raise KeyError(key)."""
@abc.abstractmethod
[docs] def setitem(self, key, value): """Set an item on the shelf, with the given value."""
@abc.abstractmethod
[docs] def delitem(self, key): """Remove an item from the shelf."""
@abc.abstractmethod
[docs] def clear(self): """Remove all items from the shelf."""
[docs] def unpack(self, key, value): """Unpack value from ``getitem``. This is useful for `Shelf` implementations which require metadata be stored with the shelved values, in which case ``pack`` should implement the inverse operation. By default, the value is simply passed through without modification. The ``unpack`` implementation is called on ``__getitem__`` and therefore can raise `KeyError` if packed metadata indicates that a value is invalid. """ return value
[docs] def pack(self, key, value): """Pack value given to ``setitem``, inverse of ``unpack``.""" return value
def __getitem__(self, key): return self.unpack(key, self.getitem(self.__keytransform__(key))) def __setitem__(self, key, value): self.setitem(self.__keytransform__(key), self.pack(key, value)) def __delitem__(self, key): self.delitem(self.__keytransform__(key)) def __keytransform__(self, key): return key def __iter__(self): raise NotImplementedError('Shelf instances do not support iteration.') def __len__(self): raise NotImplementedError('Shelf instances do not support iteration.')
[docs]class FreshPacker(object): """Mixin for pack/unpack implementation to expire shelf content.""" #: Values are no longer fresh after this value, in seconds. expire_after = 5 * 60
[docs] def unpack(self, key, value): """Unpack and return value only if it is fresh.""" value, freshness = value if not self.is_fresh(freshness): raise KeyError('{} (stale)'.format(key)) return value
[docs] def pack(self, key, value): """Pack value with metadata on its freshness.""" return value, self.freshness()
[docs] def set_expiration(self, expire_after): """Set a new expiration for freshness of all unpacked values.""" self.expire_after = expire_after
[docs] def freshness(self): """Clock function to use for freshness packing/unpacking.""" return time.time()
[docs] def is_fresh(self, freshness): """Return False if given freshness value has expired, else True.""" if self.expire_after is None: return True return self.freshness() - freshness <= self.expire_after
[docs]class LRUShelf(Shelf): """An in-memory Least-Recently Used shelf up to `maxsize`..""" def __init__(self, maxsize=1000): self.store = LRUCache(int(maxsize)) def getitem(self, key): value = self.store.get(key, UNSET) if value is UNSET: raise KeyError(key) return value def setitem(self, key, value): self.store.put(key, value) def delitem(self, key): self.store.invalidate(key) def clear(self): self.store.clear()
[docs]class FreshLRUShelf(FreshPacker, LRUShelf): """A Least-Recently Used shelf which expires values."""
[docs]class ElasticsearchShelf(Shelf): """A shelf implemented using an elasticsearch index.""" def __init__(self, index='shelf', doc_type='shelf', **elasticsearch_init): self.es = elasticsearch.Elasticsearch(**elasticsearch_init) self.index_client = elasticsearch.client.IndicesClient(self.es) self.index = index self.doc_type = doc_type def getitem(self, key): try: doc = self.es.get(index=self.index, doc_type=self.doc_type, id=key) except elasticsearch.exceptions.NotFoundError: raise KeyError(key) if not doc: raise KeyError(key) try: value = doc['_source']['value'] except KeyError: raise KeyError('{} (malformed data)'.format(key)) return value def setitem(self, key, value): self.es.index( index=self.index, doc_type=self.doc_type, id=key, body={'value': value}, refresh=True) def delitem(self, key): self.es.delete(index=self.index, doc_type=self.doc_type, id=key) def clear(self): self.index_client.delete(self.index)
[docs]class FreshElasticsearchShelf(FreshPacker, ElasticsearchShelf): """An shelf implementation with elasticsearch which expires values."""