#!/usr/bin/env python # -*- coding: utf-8 -*- #----------------------- # Name: cache.py # Python Library # Author: Raymond Wagner # Purpose: Caching framework to store TMDb API results #----------------------- from tmdb_exceptions import * from cache_engine import Engines import cache_null import cache_file class Cache( object ): """ This class implements a persistent cache, backed in a file specified in the object creation. The file is protected for safe, concurrent access by multiple instances using flock. This cache uses JSON for speed and storage efficiency, so only simple data types are supported. Data is stored in a simple format {key:(expiretimestamp, data)} """ def __init__(self, engine=None, *args, **kwargs): self._engine = None self._data = {} self._age = 0 self.configure(engine, *args, **kwargs) def _import(self, data=None): if data is None: data = self._engine.get(self._age) for obj in sorted(data, key=lambda x: x.creation): if not obj.expired: self._data[obj.key] = obj self._age = max(self._age, obj.creation) def _expire(self): for k,v in self._data.items(): if v.expired: del self._data[k] def configure(self, engine, *args, **kwargs): if engine is None: engine = 'file' elif engine not in Engines: raise TMDBCacheError("Invalid cache engine specified: "+engine) self._engine = Engines[engine](self) self._engine.configure(*args, **kwargs) def put(self, key, data, lifetime=60*60*12): # pull existing data, so cache will be fresh when written back out if self._engine is None: raise TMDBCacheError("No cache engine configured") self._expire() self._import(self._engine.put(key, data, lifetime)) def get(self, key): if self._engine is None: raise TMDBCacheError("No cache engine configured") self._expire() if key not in self._data: self._import() try: return self._data[key].data except: return None def cached(self, callback): """ Returns a decorator that uses a callback to specify the key to use for caching the responses from the decorated function. """ return self.Cached(self, callback) class Cached( object ): def __init__(self, cache, callback, func=None, inst=None): self.cache = cache self.callback = callback self.func = func self.inst = inst if func: self.__module__ = func.__module__ self.__name__ = func.__name__ self.__doc__ = func.__doc__ def __call__(self, *args, **kwargs): if self.func is None: # decorator is waiting to be given a function if len(kwargs) or (len(args) != 1): raise TMDBCacheError('Cache.Cached decorator must be called '+\ 'a single callable argument before it '+\ 'be used.') elif args[0] is None: raise TMDBCacheError('Cache.Cached decorator called before '+\ 'being given a function to wrap.') elif not callable(args[0]): raise TMDBCacheError('Cache.Cached must be provided a '+\ 'callable object.') return self.__class__(self.cache, self.callback, args[0]) elif self.inst.lifetime == 0: return self.func(*args, **kwargs) else: key = self.callback() data = self.cache.get(key) if data is None: data = self.func(*args, **kwargs) if hasattr(self.inst, 'lifetime'): self.cache.put(key, data, self.inst.lifetime) else: self.cache.put(key, data) return data def __get__(self, inst, owner): if inst is None: return self func = self.func.__get__(inst, owner) callback = self.callback.__get__(inst, owner) return self.__class__(self.cache, callback, func, inst)