Skip to content

Instantly share code, notes, and snippets.

@jokull
Created December 10, 2011 16:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jokull/1455583 to your computer and use it in GitHub Desktop.
Save jokull/1455583 to your computer and use it in GitHub Desktop.
Tumblr CachedResponse for restkit
# 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