oauth2decorator, reviewed in http://codereview.appspot.com/4524063/
diff --git a/README b/README
index 8ace1c2..c68d441 100644
--- a/README
+++ b/README
@@ -42,3 +42,7 @@
 Depending on your version of Python, these libraries may also be installed:
 
 http://pypi.python.org/pypi/simplejson/
+
+For developement you will also need:
+
+http://pythonpaste.org/webtest/
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)
+
+#
diff --git a/runtests.py b/runtests.py
index d331e01..a09a6c0 100644
--- a/runtests.py
+++ b/runtests.py
@@ -6,6 +6,8 @@
 import unittest
 from trace import fullmodname
 
+APP_ENGINE_PATH='../google_appengine'
+
 # Conditional import of cleanup function
 try:
   from tests.utils import cleanup
@@ -15,6 +17,7 @@
 
 # Ensure current working directory is in path
 sys.path.insert(0, os.getcwd())
+sys.path.insert(0, APP_ENGINE_PATH)
 
 def build_suite(folder, verbosity):
   # find all of the test modules
@@ -60,6 +63,8 @@
   elif verbosity == 2:
     logging.basicConfig(level=logging.DEBUG)
 
+  import dev_appserver
+  dev_appserver.fix_sys_path()
   # Allow user to run a specific folder of tests
   if 'tests' in sys.argv:
     run('tests', verbosity, exit_on_failure)
diff --git a/samples/new_project_template/app.yaml b/samples/new_project_template/app.yaml
index 33f1a19..c0d43d5 100644
--- a/samples/new_project_template/app.yaml
+++ b/samples/new_project_template/app.yaml
@@ -4,6 +4,9 @@
 api_version: 1
 
 handlers:
+- url: /oauth2callback
+  script: oauth2client/appengine.py
+
 - url: .*
   script: main.py
 
diff --git a/samples/new_project_template/grant.html b/samples/new_project_template/grant.html
new file mode 100644
index 0000000..b199ea0
--- /dev/null
+++ b/samples/new_project_template/grant.html
@@ -0,0 +1,17 @@
+<html>
+  <head>
+    <title>Can Haz Perms?</title>
+  </head>
+  <body>
+    {% if has_credentials %}
+    <p>Thanks for granting us permission. Please <a href="/">proceed to the main
+      application</a>.</p>
+    {% else %}
+    <p><a href="{{ url }}">Grant</a> this application permission to read your
+    Buzz information and it will let you know how many followers you have.</p>
+    {% endif %}
+    <p>You can always <a
+      href="https://www.google.com/accounts/b/0/IssuedAuthSubTokens">revoke</a>
+    permission at any time.</p>
+  </body>
+</html>
diff --git a/samples/new_project_template/main.py b/samples/new_project_template/main.py
index c451e67..520fbb4 100755
--- a/samples/new_project_template/main.py
+++ b/samples/new_project_template/main.py
@@ -31,100 +31,64 @@
 import pickle
 
 from apiclient.discovery import build
-from oauth2client.appengine import CredentialsProperty
-from oauth2client.appengine import StorageByKeyName
-from oauth2client.client import OAuth2WebServerFlow
+from oauth2client.appengine import OAuth2Decorator
+from oauth2client.client import AccessTokenRefreshError
 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 import template
-from google.appengine.ext.webapp import util
-from google.appengine.ext.webapp.util import login_required
+from google.appengine.ext.webapp.util import run_wsgi_app
 
-# Set up a Flow object to be used if we need to authenticate. This
-# sample uses OAuth 2.0, and we set up the OAuth2WebServerFlow with
-# the information it needs to authenticate. Note that it is called
-# the Web Server Flow, but it can also handle the flow for native
-# applications <http://code.google.com/apis/accounts/docs/OAuth2.html#IA>
-# The client_id and client_secret are copied from the Identity tab on
+# The client_id and client_secret are copied from the API Access tab on
 # the Google APIs Console <http://code.google.com/apis/console>
-
-FLOW = OAuth2WebServerFlow(
-    client_id='<client id goes here>',
-    client_secret='<client secret goes here>',
+decorator = OAuth2Decorator(
+    client_id='837647042410-75ifgipj95q4agpm0cs452mg7i2pn17c.apps.googleusercontent.com',
+    client_secret='QhxYsjM__u4vy5N0DXUFRwwI',
     scope='https://www.googleapis.com/auth/buzz',
     user_agent='my-sample-app/1.0')
 
 
-class Credentials(db.Model):
-  credentials = CredentialsProperty()
-
-
 class MainHandler(webapp.RequestHandler):
 
-  @login_required
+  @decorator.oauth_required
   def get(self):
-    user = users.get_current_user()
-    credentials = StorageByKeyName(
-        Credentials, user.user_id(), 'credentials').get()
-
-    if not credentials or credentials.invalid:
-      return begin_oauth_flow(self, user)
-
-    http = credentials.authorize(httplib2.Http())
+    http = decorator.http()
 
     # Build a service object for interacting with the API. Visit
     # the Google APIs Console <http://code.google.com/apis/console>
     # to get a developerKey for your own application.
-    service = build("buzz", "v1", http=http)
-    followers = service.people().list(
-        userId='@me', groupId='@followers').execute()
-    text = 'Hello, you have %s followers!' % followers['totalResults']
+    try:
+      service = build("buzz", "v1", http=http)
+      followers = service.people().list(
+          userId='@me', groupId='@followers').execute()
+      text = 'Hello, you have %s followers!' % followers['totalResults']
 
-    path = os.path.join(os.path.dirname(__file__), 'welcome.html')
-    self.response.out.write(template.render(path, {'text': text }))
+      path = os.path.join(os.path.dirname(__file__), 'welcome.html')
+      self.response.out.write(template.render(path, {'text': text }))
+    except AccessTokenRefreshError:
+      self.redirect('/grant')
 
 
-def begin_oauth_flow(request_handler, user):
-  callback = request_handler.request.relative_url('/oauth2callback')
-  authorize_url = FLOW.step1_get_authorize_url(callback)
-  # Here we are using memcache to store the flow temporarily while the user
-  # is directed to authorize our service. You could also store the flow
-  # in the datastore depending on your utilization of memcache, just remember
-  # in that case to clean up the flow after you are done with it.
-  memcache.set(user.user_id(), pickle.dumps(FLOW))
-  request_handler.redirect(authorize_url)
+class GrantHandler(webapp.RequestHandler):
 
-
-class OAuthHandler(webapp.RequestHandler):
-
-  @login_required
+  @decorator.oauth_aware
   def get(self):
-    user = users.get_current_user()
-    flow = pickle.loads(memcache.get(user.user_id()))
-    # 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(
-          Credentials, user.user_id(), 'credentials').put(credentials)
-      self.redirect("/")
-    else:
-      # Add application specific error handling here.
-      pass
-
+    path = os.path.join(os.path.dirname(__file__), 'grant.html')
+    variables = {
+        'url': decorator.authorize_url(),
+        'has_credentials': decorator.has_credentials()
+        }
+    self.response.out.write(template.render(path, variables))
 
 def main():
   application = webapp.WSGIApplication(
       [
-      ('/', MainHandler),
-      ('/oauth2callback', OAuthHandler)
+       ('/', MainHandler),
+       ('/grant', GrantHandler),
       ],
       debug=True)
-  util.run_wsgi_app(application)
+  run_wsgi_app(application)
 
 
 if __name__ == '__main__':
diff --git a/samples/new_project_template/welcome.html b/samples/new_project_template/welcome.html
index 0117f50..57b186e 100644
--- a/samples/new_project_template/welcome.html
+++ b/samples/new_project_template/welcome.html
@@ -1,6 +1,6 @@
 <html>
   <head>
-    <title>Bootcamp Translations</title>
+    <title>Welcome</title>
   </head>
   <body>
     <p>{{ text }}</p>
diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py
new file mode 100644
index 0000000..208a3f5
--- /dev/null
+++ b/tests/test_oauth2client_appengine.py
@@ -0,0 +1,176 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""Discovery document tests
+
+Unit tests for objects created from discovery documents.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import httplib2
+import unittest
+import urlparse
+
+try:
+    from urlparse import parse_qs
+except ImportError:
+    from cgi import parse_qs
+
+from apiclient.http import HttpMockSequence
+from apiclient.anyjson import simplejson
+from webtest import TestApp
+from oauth2client.client import AccessTokenRefreshError
+from oauth2client.client import FlowExchangeError
+from oauth2client.appengine import OAuth2Decorator
+from google.appengine.ext import webapp
+from google.appengine.api import users
+from oauth2client.appengine import OAuth2Handler
+from google.appengine.ext import testbed
+
+
+class UserMock(object):
+  """Mock the app engine user service"""
+  def user_id(self):
+    return 'foo_user'
+
+
+class Http2Mock(object):
+  """Mock httplib2.Http"""
+  status = 200
+  content = {
+      'access_token': 'foo_access_token',
+      'refresh_token': 'foo_refresh_token',
+      'expires_in': 3600
+    }
+
+  def request(self, token_uri, method, body, headers, *args, **kwargs):
+    self.body = body
+    self.headers = headers
+    return (self, simplejson.dumps(self.content))
+
+
+class DecoratorTests(unittest.TestCase):
+
+  def setUp(self):
+    self.testbed = testbed.Testbed()
+    self.testbed.activate()
+    self.testbed.init_datastore_v3_stub()
+    self.testbed.init_memcache_stub()
+    self.testbed.init_user_stub()
+
+    decorator = OAuth2Decorator(client_id='foo_client_id',
+                                client_secret='foo_client_secret',
+                                scope='foo_scope',
+                                user_agent='foo_user_agent')
+    self.decorator = decorator
+
+
+    class TestRequiredHandler(webapp.RequestHandler):
+      @decorator.oauth_required
+      def get(self):
+        pass
+
+
+    class TestAwareHandler(webapp.RequestHandler):
+      @decorator.oauth_aware
+      def get(self):
+        self.response.out.write('Hello World!')
+
+
+    application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler),
+                                          ('/foo_path', TestRequiredHandler),
+                                          ('/bar_path', TestAwareHandler)],
+                                         debug=True)
+    self.app = TestApp(application)
+    users.get_current_user = UserMock
+    httplib2.Http = Http2Mock
+
+  def tearDown(self):
+    self.testbed.deactivate()
+
+  def test_required(self):
+    # An initial request to an oauth_required decorated path should be a
+    # redirect to start the OAuth dance.
+    response = self.app.get('/foo_path')
+    self.assertTrue(response.status.startswith('302'))
+    q = parse_qs(response.headers['Location'].split('?', 1)[1])
+    self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
+    self.assertEqual('foo_client_id', q['client_id'][0])
+    self.assertEqual('foo_scope', q['scope'][0])
+    self.assertEqual('http://localhost/foo_path', q['state'][0])
+    self.assertEqual('code', q['response_type'][0])
+    self.assertEqual(False, self.decorator.has_credentials())
+
+    # Now simulate the callback to /oauth2callback
+    response = self.app.get('/oauth2callback', {
+        'code': 'foo_access_code',
+        'state': 'foo_path'
+        })
+    self.assertEqual('http://localhost/foo_path', response.headers['Location'])
+    self.assertEqual(None, self.decorator.credentials)
+
+    # Now requesting the decorated path should work
+    response = self.app.get('/foo_path')
+    self.assertEqual('200 OK', response.status)
+    self.assertEqual(True, self.decorator.has_credentials())
+    self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token)
+    self.assertEqual('foo_access_token', self.decorator.credentials.access_token)
+
+    # Invalidate the stored Credentials
+    self.decorator.credentials._invalid = True
+    self.decorator.credentials.store(self.decorator.credentials)
+
+    # Invalid Credentials should start the OAuth dance again
+    response = self.app.get('/foo_path')
+    self.assertTrue(response.status.startswith('302'))
+    q = parse_qs(response.headers['Location'].split('?', 1)[1])
+    self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
+
+  def test_aware(self):
+    # An initial request to an oauth_aware decorated path should not redirect
+    response = self.app.get('/bar_path')
+    self.assertEqual('Hello World!', response.body)
+    self.assertEqual('200 OK', response.status)
+    self.assertEqual(False, self.decorator.has_credentials())
+    url = self.decorator.authorize_url()
+    q = parse_qs(url.split('?', 1)[1])
+    self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
+    self.assertEqual('foo_client_id', q['client_id'][0])
+    self.assertEqual('foo_scope', q['scope'][0])
+    self.assertEqual('http://localhost/bar_path', q['state'][0])
+    self.assertEqual('code', q['response_type'][0])
+
+    # Now simulate the callback to /oauth2callback
+    url = self.decorator.authorize_url()
+    response = self.app.get('/oauth2callback', {
+        'code': 'foo_access_code',
+        'state': 'bar_path'
+        })
+    self.assertEqual('http://localhost/bar_path', response.headers['Location'])
+    self.assertEqual(False, self.decorator.has_credentials())
+
+    # Now requesting the decorated path will have credentials
+    response = self.app.get('/bar_path')
+    self.assertEqual('200 OK', response.status)
+    self.assertEqual('Hello World!', response.body)
+    self.assertEqual(True, self.decorator.has_credentials())
+    self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token)
+    self.assertEqual('foo_access_token', self.decorator.credentials.access_token)
+
+if __name__ == '__main__':
+  unittest.main()