Skip to content

Commit 3ae264c

Browse files
committed
Added debug cache
1 parent 431941b commit 3ae264c

File tree

3 files changed

+166
-1
lines changed

3 files changed

+166
-1
lines changed

sunlight/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import warnings
2929
import sunlight.config
3030
import sunlight.service
31+
import sunlight.debugcache
3132

3233

3334
def available_services():
@@ -64,3 +65,6 @@ def _attempt_to_load_apikey():
6465
pass
6566

6667
_attempt_to_load_apikey()
68+
69+
70+
cache = sunlight.debugcache.debug_cache

sunlight/debugcache.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import pickle
2+
import logging
3+
import functools
4+
5+
6+
backends = {}
7+
8+
class _BackendMeta(type):
9+
def __new__(meta, name, bases, attrs):
10+
cls = type.__new__(meta, name, bases, attrs)
11+
backends[name] = cls
12+
shortname = name.lower().replace('backend', '')
13+
backends[shortname] = cls
14+
for nickname in attrs.get('nicknames', []):
15+
backends[nickname] = cls
16+
return cls
17+
18+
19+
class BaseBackend(object):
20+
__metaclass__ = _BackendMeta
21+
22+
def check(self, key):
23+
'''Try to return something from the cache.
24+
'''
25+
raise NotImplementedError()
26+
27+
def set(self, key, val):
28+
'''Set something in the cache.
29+
'''
30+
raise NotImplementedError()
31+
32+
def purge(self, *keys):
33+
'''Try to purge one or more things from the cache.
34+
If keys is empty, purges everything.
35+
'''
36+
raise NotImplementedError()
37+
38+
39+
class MemoryBackend(BaseBackend):
40+
'''In memory cache for API responses.
41+
'''
42+
nicknames = ['mem', 'locmem', 'localmem']
43+
44+
def __init__(self):
45+
self._cache = {}
46+
47+
def check(self, key):
48+
return self._cache.get(key)
49+
50+
def set(self, key, val):
51+
self._cache[key] = val
52+
53+
def purge(self, *keys):
54+
if not keys:
55+
self._cache = {}
56+
map(self._cache.pop, keys)
57+
58+
59+
class MongoBackend(BaseBackend):
60+
'''Mongo cache of API respones.
61+
'''
62+
def __init__(self):
63+
self.mongo = get_mongo()
64+
65+
def check(self, key):
66+
doc = self.mongo.responses.find_one(key)
67+
if doc:
68+
return doc['v']
69+
70+
def set(self, key, val):
71+
doc = dict(_id=key, v=val)
72+
self.mongo.responses.save(doc)
73+
74+
def purge(self, *keys):
75+
if not keys:
76+
return self.mongo.drop_database()
77+
self.mongo.reponses.remove({'_id': {'$in': keys}})
78+
79+
80+
class BaseCache(object):
81+
82+
def __init__(self):
83+
self.backend = None
84+
logging.basicConfig(
85+
format='%(asctime)s %(levelname)s %(message)s')
86+
self.logger = logging.getLogger('cache')
87+
88+
def disable(self):
89+
'''Disable the cache. Will wipe out an in-memory cache.
90+
'''
91+
self.backend = None
92+
self.logger.info('Response caching disabled.')
93+
94+
def purge(self):
95+
'''Wipe out the cache.
96+
'''
97+
self.logger.info('Purging cache...')
98+
self.backend.purge()
99+
self.logger.info('...done.')
100+
101+
def set_backend(self, backend_name):
102+
try:
103+
self.backend = backends[backend_name]()
104+
self.logger.info('Changed cache backend to %r.' % self.backend)
105+
except KeyError:
106+
raise ValueError('No backend named %r is defined.' % backend_name)
107+
108+
enable = set_backend
109+
110+
def get_key(self, *args, **kwargs):
111+
'''Create a cache key based on the input to the wrapped callable.
112+
'''
113+
raise NotImplementedError()
114+
115+
def __call__(self, method):
116+
'''Returns a class decorator.
117+
'''
118+
cache = self
119+
@functools.wraps(method)
120+
def memoizer(self, *args, **kwargs):
121+
# If no backend is set, do nothing.
122+
if cache.backend is None:
123+
return method(self, *args, **kwargs)
124+
key = cache.get_key(self, *args, **kwargs)
125+
val = cache.backend.check(key)
126+
if val is None:
127+
cache.logger.debug(' MISS %r' % [self, args, kwargs])
128+
val = method(self, *args, **kwargs)
129+
cache.backend.set(key, val)
130+
else:
131+
cache.logger.debug(' HIT %r' % [self, args, kwargs])
132+
return val
133+
return memoizer
134+
135+
136+
class ResponseCache(BaseCache):
137+
138+
def get_key(self, method_self, *args, **kwargs):
139+
name = self.__class__.__name__
140+
module = self.__class__.__module__
141+
key = pickle.dumps((module, name, args, kwargs))
142+
return key
143+
144+
145+
debug_cache = ResponseCache()
146+
147+
148+
def get_mongo():
149+
try:
150+
import pymongo
151+
except ImportError:
152+
msg = 'The mongo cache backend requires pymongo.'
153+
raise ImportError(msg)
154+
from sunlight import config
155+
host = getattr(config, 'MONGO_HOST', None)
156+
dbname = getattr(config, 'MONGO_DATABASE_NAME', 'pythonsunlight_cache')
157+
conn = pymongo.MongoClient(host=host)
158+
mongo = getattr(conn, dbname)
159+
return mongo

sunlight/service.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import sunlight.config
1414
import sunlight.errors
15+
from sunlight.debugcache import debug_cache
16+
1517

1618
if sys.version_info[0] >= 3:
1719
from urllib.parse import urlencode
@@ -42,7 +44,7 @@ class Service:
4244
Base class for all the API implementations, as well as a bunch of common
4345
code on how to actually fetch text over the network.
4446
"""
45-
47+
@debug_cache
4648
def get(self, top_level_object, **kwargs):
4749
"""
4850
Get some data from the network - this is where we actually fetch

0 commit comments

Comments
 (0)