oauth2decorator, reviewed in http://codereview.appspot.com/4524063/
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index f939bc0..5600387 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -19,13 +19,22 @@
 
 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
 
+import httplib2
 import pickle
 
-from google.appengine.ext import db
+from client import AccessTokenRefreshError
 from client import Credentials
 from client import Flow
+from client import OAuth2WebServerFlow
 from client import Storage
+from google.appengine.api import memcache
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import login_required
+from google.appengine.ext.webapp.util import run_wsgi_app
 
+OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
 
 class FlowProperty(db.Property):
   """App Engine datastore Property for Flow.
@@ -102,17 +111,19 @@
   are stored by key_name.
   """
 
-  def __init__(self, model, key_name, property_name):
+  def __init__(self, model, key_name, property_name, cache=None):
     """Constructor for Storage.
 
     Args:
       model: db.Model, model class
       key_name: string, key name for the entity that has the credentials
       property_name: string, name of the property that is an CredentialsProperty
+      cache: memcache, a write-through cache to put in front of the datastore
     """
     self._model = model
     self._key_name = key_name
     self._property_name = property_name
+    self._cache = cache
 
   def get(self):
     """Retrieve Credential from datastore.
@@ -120,10 +131,17 @@
     Returns:
       oauth2client.Credentials
     """
+    if self._cache:
+      credential = self._cache.get(self._key_name)
+      if credential:
+        return pickle.loads(credential)
     entity = self._model.get_or_insert(self._key_name)
     credential = getattr(entity, self._property_name)
     if credential and hasattr(credential, 'set_store'):
       credential.set_store(self.put)
+      if self._cache:
+        self._cache.set(self._key_name, pickle.dumps(credentials))
+
     return credential
 
   def put(self, credentials):
@@ -135,3 +153,174 @@
     entity = self._model.get_or_insert(self._key_name)
     setattr(entity, self._property_name, credentials)
     entity.put()
+    if self._cache:
+      self._cache.set(self._key_name, pickle.dumps(credentials))
+
+
+class CredentialsModel(db.Model):
+  """Storage for OAuth 2.0 Credentials
+
+  Storage of the model is keyed by the user.user_id().
+  """
+  credentials = CredentialsProperty()
+
+
+class OAuth2Decorator(object):
+  """Utility for making OAuth 2.0 easier.
+
+  Instantiate and then use with oauth_required or oauth_aware
+  as decorators on webapp.RequestHandler methods.
+
+  Example:
+
+    decorator = OAuth2Decorator(
+        client_id='837...ent.com',
+        client_secret='Qh...wwI',
+        scope='https://www.googleapis.com/auth/buzz',
+        user_agent='my-sample-app/1.0')
+
+
+    class MainHandler(webapp.RequestHandler):
+
+      @decorator.oauth_required
+      def get(self):
+        http = decorator.http()
+        # http is authorized with the user's Credentials and can be used
+        # in API calls
+
+  """
+  def __init__(self, client_id, client_secret, scope, user_agent,
+               auth_uri='https://accounts.google.com/o/oauth2/auth',
+               token_uri='https://accounts.google.com/o/oauth2/token'):
+
+    """Constructor for OAuth2Decorator
+
+    Args:
+      client_id: string, client identifier.
+      client_secret: string client secret.
+      scope: string, scope of the credentials being requested.
+      user_agent: string, HTTP User-Agent to provide for this application.
+      auth_uri: string, URI for authorization endpoint. For convenience
+        defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+      token_uri: string, URI for token endpoint. For convenience
+        defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+    """
+    self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
+      auth_uri, token_uri)
+    self.credentials = None
+    self._request_handler = None
+
+  def oauth_required(self, method):
+    """Decorator that starts the OAuth 2.0 dance.
+
+    Starts the OAuth dance for the logged in user if they haven't already
+    granted access for this application.
+
+    Args:
+      method: callable, to be decorated method of a webapp.RequestHandler
+        instance.
+    """
+    @login_required
+    def check_oauth(request_handler, *args):
+      # Store the request URI in 'state' so we can use it later
+      self.flow.params['state'] = request_handler.request.url
+      self._request_handler = request_handler
+      user = users.get_current_user()
+      self.credentials = StorageByKeyName(
+          CredentialsModel, user.user_id(), 'credentials').get()
+
+      if not self.has_credentials():
+        return request_handler.redirect(self.authorize_url())
+      try:
+        method(request_handler, *args)
+      except AccessTokenRefreshError:
+        return request_handler.redirect(self.authorize_url())
+
+    return check_oauth
+
+  def oauth_aware(self, method):
+    """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
+
+    Does all the setup for the OAuth dance, but doesn't initiate it.
+    This decorator is useful if you want to create a page that knows
+    whether or not the user has granted access to this application.
+    From within a method decorated with @oauth_aware the has_credentials()
+    and authorize_url() methods can be called.
+
+    Args:
+      method: callable, to be decorated method of a webapp.RequestHandler
+        instance.
+    """
+    @login_required
+    def setup_oauth(request_handler, *args):
+      self.flow.params['state'] = request_handler.request.url
+      self._request_handler = request_handler
+      user = users.get_current_user()
+      self.credentials = StorageByKeyName(
+          CredentialsModel, user.user_id(), 'credentials').get()
+      method(request_handler, *args)
+    return setup_oauth
+
+  def has_credentials(self):
+    """True if for the logged in user there are valid access Credentials.
+
+    Must only be called from with a webapp.RequestHandler subclassed method
+    that had been decorated with either @oauth_required or @oauth_aware.
+    """
+    return self.credentials is not None and not self.credentials.invalid
+
+  def authorize_url(self):
+    """Returns the URL to start the OAuth dance.
+
+    Must only be called from with a webapp.RequestHandler subclassed method
+    that had been decorated with either @oauth_required or @oauth_aware.
+    """
+    callback = self._request_handler.request.relative_url('/oauth2callback')
+    url = self.flow.step1_get_authorize_url(callback)
+    user = users.get_current_user()
+    memcache.set(user.user_id(), pickle.dumps(self.flow),
+                 namespace=OAUTH2CLIENT_NAMESPACE)
+    return url
+
+  def http(self):
+    """Returns an authorized http instance.
+
+    Must only be called from within an @oauth_required decorated method, or
+    from within an @oauth_aware decorated method where has_credentials()
+    returns True.
+    """
+    return self.credentials.authorize(httplib2.Http())
+
+
+class OAuth2Handler(webapp.RequestHandler):
+  """Handler for the redirect_uri of the OAuth 2.0 dance."""
+
+  @login_required
+  def get(self):
+    error = self.request.get('error')
+    if error:
+      errormsg = self.request.get('error_description', error)
+      self.response.out.write('The authorization request failed: %s' % errormsg)
+    else:
+      user = users.get_current_user()
+      flow = pickle.loads(memcache.get(user.user_id(), namespace=OAUTH2CLIENT_NAMESPACE))
+      # This code should be ammended with application specific error
+      # handling. The following cases should be considered:
+      # 1. What if the flow doesn't exist in memcache? Or is corrupt?
+      # 2. What if the step2_exchange fails?
+      if flow:
+        credentials = flow.step2_exchange(self.request.params)
+        StorageByKeyName(
+            CredentialsModel, user.user_id(), 'credentials').put(credentials)
+        self.redirect(self.request.get('state'))
+      else:
+        # TODO Add error handling here.
+        pass
+
+
+application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
+
+def main():
+  run_wsgi_app(application)
+
+#