Created
December 10, 2011 16:55
-
-
Save jokull/1455583 to your computer and use it in GitHub Desktop.
Tumblr CachedResponse for restkit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding=utf-8 | |
""" | |
Use Tumblr as a backend for a blog. This module | |
helps interfacing with their APIv2 in a pythonic way. Caching | |
keeps things responsive and guards against API downtime and | |
response failures. | |
Assumptions: | |
Flask with a `Redis` object on `g.redis`. All actions are | |
within a Flask request context. | |
Requirements: | |
restkit | |
flask | |
""" | |
import json, time, functools | |
from hashlib import md5 | |
from flask import g, current_app | |
from restkit import Resource | |
from http_parser.http import ParserError as HttpParserError | |
from restkit.errors import (ParserError, ResourceError, | |
RequestTimeout, RequestFailed, RequestError) | |
class CachedResponse(object): | |
"""Storing, retrieving, purging of Tumblr API Responses. | |
This is done to speed up the site and remain responsive | |
when Tumblr goes down. | |
Caching logic is permissive of API faults | |
# Check if too old | |
# ok -> deliver | |
# not ok -> try to grab | |
# if succesful -> deliver | |
# if not succesful -> deliver old, reset timestamp | |
""" | |
TIMESTAMP_KEY = 'andriki:response:timestamp:%s:%s' | |
CONTENT_KEY = 'andriki:response:content:%s:%s' | |
CACHE_TTL = 60 * 1 # One minute | |
TOLERATED_ERRORS = (ParserError, ResourceError, | |
RequestTimeout, RequestFailed, RequestError, | |
HttpParserError) | |
EMPTY_RESPONSE = dict(response='', meta=dict()) | |
def __init__(self, path, params, resource): | |
self.path, self.params = path, params | |
self.server = functools.partial(Resource.get, resource, self.path, **self.params) | |
timestamp = self.timestamp or 0 | |
content = self.content | |
stale_cache = (time.time() - timestamp) > self.CACHE_TTL | |
if stale_cache or not content: | |
self.set() | |
def get_param_hash(self): | |
param_hash = md5() | |
for key, value in sorted(self.params.items()): | |
param_hash.update(u'%s:%s' % (key, value)) | |
return param_hash.hexdigest() | |
def set(self): | |
self.timestamp = 'now' | |
try: | |
req = self.server() | |
except self.TOLERATED_ERRORS, e: | |
current_app.logger.warning(str(e)) | |
# Leave content untouched, but update the TTL so | |
# that if Tumblr API is down we're not hammering it | |
else: | |
self.content = req.body_string() | |
def delete(self): | |
del self.content | |
del self.timestamp | |
def body_string(self): | |
"""Because we want similar API as restkit `Response` | |
""" | |
return self.content or json.dumps(self.EMPTY_RESPONSE) | |
# Setters and getters for both content and timestamp keys | |
@property | |
def timestamp_key(self): | |
return self.TIMESTAMP_KEY % (self.path, self.get_param_hash()) | |
def _get_timestamp(self): | |
value = g.redis.get(self.timestamp_key) | |
return value and float(value) or None | |
def _set_timestamp(self, value='now'): | |
if value == 'now': | |
value = time.time() | |
return g.redis.set(self.timestamp_key, value) | |
def _delete_timestamp(self): | |
return g.redis.delete(self.timestamp_key) | |
timestamp = property(_get_timestamp, _set_timestamp, _delete_timestamp) | |
@property | |
def content_key(self): | |
return self.CONTENT_KEY % (self.path, self.get_param_hash()) | |
def _get_content(self): | |
return g.redis.get(self.content_key) | |
def _set_content(self, value): | |
if value: | |
g.redis.set(self.content_key, value) | |
def _delete_content(self): | |
return g.redis.delete(self.content_key) | |
content = property(_get_content, _set_content, _delete_content) | |
class Tumblr(Resource): | |
ENDPOINT = 'http://api.tumblr.com/v2/blog/%s' | |
def __init__(self, blog_url, api_key=None, **kwargs): | |
self.params = dict() | |
if api_key: | |
self.params['api_key'] = api_key | |
Resource.__init__(self, self.ENDPOINT % blog_url, | |
follow_redirect=True, | |
max_follow_redirect=10, **kwargs) | |
def get(self, path, **params): | |
return CachedResponse(path, params, self) | |
@classmethod | |
def process(cls, response): | |
data = json.loads(response.body_string()) | |
return data['response'] | |
def posts(self, **params): | |
params.update(**self.params) | |
resp = self.get('/posts', **params) | |
return self.process(resp)['posts'] | |
def info(self): | |
resp = self.get('/info', **self.params) | |
return self.process(resp) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment