|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
#-----------------------
|
|
|
|
# Name: cache.py
|
|
|
|
# Python Library
|
|
|
|
# Author: Raymond Wagner
|
|
|
|
# Purpose: Caching framework to store TMDb API results
|
|
|
|
#-----------------------
|
|
|
|
|
|
|
|
import time
|
|
|
|
import os
|
|
|
|
|
|
|
|
from tmdb_exceptions import *
|
|
|
|
from cache_engine import Engines
|
|
|
|
|
|
|
|
import cache_null
|
|
|
|
import cache_file
|
|
|
|
|
|
|
|
|
|
|
|
class Cache(object):
|
|
|
|
"""
|
|
|
|
This class implements a cache framework, allowing selecting of a
|
|
|
|
pluggable engine. The framework stores data in a key/value manner,
|
|
|
|
along with a lifetime, after which data will be expired and
|
|
|
|
pulled fresh next time it is requested from the cache.
|
|
|
|
|
|
|
|
This class defines a wrapper to be used with query functions. The
|
|
|
|
wrapper will automatically cache the inputs and outputs of the
|
|
|
|
wrapped function, pulling the output from local storage for
|
|
|
|
subsequent calls with those inputs.
|
|
|
|
"""
|
|
|
|
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:
|
|
|
|
# lifetime of zero means never cache
|
|
|
|
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)
|