Cleaned up oauth code based on feedback. Created FlowThreeLegged and OAuthCredentials classes.
diff --git a/apiclient/contrib/__init__.py b/apiclient/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apiclient/contrib/__init__.py
diff --git a/apiclient/ext/__init__.py b/apiclient/ext/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apiclient/ext/__init__.py
diff --git a/apiclient/ext/appengine.py b/apiclient/ext/appengine.py
new file mode 100644
index 0000000..a780d0e
--- /dev/null
+++ b/apiclient/ext/appengine.py
@@ -0,0 +1,90 @@
+# Copyright (C) 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.
+
+"""Utilities for Google App Engine
+
+Utilities for making it easier to use the
+Google API Client for Python on Google App Engine.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import pickle
+
+from google.appengine.ext import db
+from apiclient.oauth import OAuthCredentials
+from apiclient.oauth import FlowThreeLegged
+
+
+class FlowThreeLeggedProperty(db.Property):
+  """Utility property that allows easy
+  storage and retreival of an
+  apiclient.oauth.FlowThreeLegged"""
+
+  # Tell what the user type is.
+  data_type = FlowThreeLegged
+
+  # For writing to datastore.
+  def get_value_for_datastore(self, model_instance):
+    flow = super(FlowThreeLeggedProperty,
+                 self).get_value_for_datastore(model_instance)
+    return db.Blob(pickle.dumps(flow))
+
+  # For reading from datastore.
+  def make_value_from_datastore(self, value):
+    if value is None:
+      return None
+    return pickle.loads(value)
+
+  def validate(self, value):
+    if value is not None and not isinstance(value, FlowThreeLegged):
+      raise BadValueError('Property %s must be convertible '
+                          'to a FlowThreeLegged instance (%s)' %
+                          (self.name, value))
+    return super(FlowThreeLeggedProperty, self).validate(value)
+
+  def empty(self, value):
+    return not value
+
+
+class OAuthCredentialsProperty(db.Property):
+  """Utility property that allows easy
+  storage and retrieval of
+  apiclient.oath.OAuthCredentials
+  """
+
+  # Tell what the user type is.
+  data_type = OAuthCredentials
+
+  # For writing to datastore.
+  def get_value_for_datastore(self, model_instance):
+    cred = super(OAuthCredentialsProperty,
+                 self).get_value_for_datastore(model_instance)
+    return db.Blob(pickle.dumps(cred))
+
+  # For reading from datastore.
+  def make_value_from_datastore(self, value):
+    if value is None:
+      return None
+    return pickle.loads(value)
+
+  def validate(self, value):
+    if value is not None and not isinstance(value, OAuthCredentials):
+      raise BadValueError('Property %s must be convertible '
+                          'to an OAuthCredentials instance (%s)' %
+                          (self.name, value))
+    return super(OAuthCredentialsProperty, self).validate(value)
+
+  def empty(self, value):
+    return not value
diff --git a/apiclient/oauth.py b/apiclient/oauth.py
index c1908d1..63f896b 100644
--- a/apiclient/oauth.py
+++ b/apiclient/oauth.py
@@ -10,30 +10,25 @@
 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
 
 import copy
-import urllib
+import httplib2
 import oauth2 as oauth
+import urllib
+import logging
 
 try:
     from urlparse import parse_qs, parse_qsl
 except ImportError:
     from cgi import parse_qs, parse_qsl
+
+
 class MissingParameter(Exception):
   pass
 
-def abstract():
+
+def _abstract():
   raise NotImplementedError("You need to override this function")
 
 
-class TokenStore(object):
-  def get(user, service):
-    """Returns an oauth.Token based on the (user, service) returning
-    None if there is no Token for that (user, service).
-    """
-    abstract()
-
-  def set(user, service, token):
-    abstract()
-
 buzz_discovery = {
     'required': ['domain', 'scope'],
     'request': {
@@ -50,7 +45,19 @@
         },
     }
 
+
 def _oauth_uri(name, discovery, params):
+  """Look up the OAuth UR from the discovery
+  document and add query parameters based on
+  params.
+
+  name      - The name of the OAuth URI to lookup, one
+              of 'request', 'access', or 'authorize'.
+  discovery - Portion of discovery document the describes
+              the OAuth endpoints.
+  params    - Dictionary that is used to form the query parameters
+              for the specified URI.
+  """
   if name not in ['request', 'access', 'authorize']:
     raise KeyError(name)
   keys = []
@@ -62,9 +69,100 @@
       query[key] = params[key]
   return discovery[name]['url'] + '?' + urllib.urlencode(query)
 
-class Flow3LO(object):
+
+class Credentials(object):
+  """Base class for all Credentials objects.
+
+  Subclasses must define an authorize() method
+  that applies the credentials to an HTTP transport.
+  """
+
+  def authorize(self, http):
+    """Take an httplib2.Http instance (or equivalent) and
+    authorizes it for the set of credentials, usually by
+    replacing http.request() with a method that adds in
+    the appropriate headers and then delegates to the original
+    Http.request() method.
+    """
+    _abstract()
+
+
+class OAuthCredentials(Credentials):
+  """Credentials object for OAuth 1.0a
+  """
+
+  def __init__(self, consumer, token, user_agent):
+    """
+    consumer   - An instance of oauth.Consumer.
+    token      - An instance of oauth.Token constructed with
+                 the access token and secret.
+    user_agent - The HTTP User-Agent to provide for this application.
+    """
+    self.consumer = consumer
+    self.token = token
+    self.user_agent = user_agent
+
+  def authorize(self, http):
+    """
+    Args:
+       http - An instance of httplib2.Http
+           or something that acts like it.
+
+    Returns:
+       A modified instance of http that was passed in.
+
+    Example:
+
+      h = httplib2.Http()
+      h = credentials.authorize(h)
+
+    You can't create a new OAuth
+    subclass of httplib2.Authenication because
+    it never gets passed the absolute URI, which is
+    needed for signing. So instead we have to overload
+    'request' with a closure that adds in the
+    Authorization header and then calls the original version
+    of 'request()'.
+    """
+    request_orig = http.request
+    signer = oauth.SignatureMethod_HMAC_SHA1()
+
+    # The closure that will replace 'httplib2.Http.request'.
+    def new_request(uri, method="GET", body=None, headers=None,
+                    redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+                    connection_type=None):
+      """Modify the request headers to add the appropriate
+      Authorization header."""
+      req = oauth.Request.from_consumer_and_token(
+          self.consumer, self.token, http_method=method, http_url=uri)
+      req.sign_request(signer, self.consumer, self.token)
+      if headers == None:
+        headers = {}
+      headers.update(req.to_header())
+      if 'user-agent' not in headers:
+        headers['user-agent'] = self.user_agent
+      return request_orig(uri, method, body, headers,
+                          redirections, connection_type)
+
+    http.request = new_request
+    return http
+
+
+class FlowThreeLegged(object):
+  """Does the Three Legged Dance for OAuth 1.0a.
+  """
+
   def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
                **kwargs):
+    """
+    discovery       - Section of the API discovery document that describes
+                      the OAuth endpoints.
+    consumer_key    - OAuth consumer key
+    consumer_secret - OAuth consumer secret
+    user_agent      - The HTTP User-Agent that identifies the application.
+    **kwargs        - The keyword arguments are all optional and required
+                      parameters for the OAuth calls.
+    """
     self.discovery = discovery
     self.consumer_key = consumer_key
     self.consumer_secret = consumer_secret
@@ -75,14 +173,17 @@
       if key not in self.params:
         raise MissingParameter('Required parameter %s not supplied' % key)
 
-  def step1(self, oauth_callback='oob'):
+  def step1_get_authorize_url(self, oauth_callback='oob'):
     """Returns a URI to redirect to the provider.
 
-    If oauth_callback is 'oob' then the next call
-    should be to step2_pin, otherwise oauth_callback
-    is a URI and the next call should be to
-    step2_callback() with the query parameters
-    received at that callback.
+    oauth_callback - Either the string 'oob' for a non-web-based application,
+                     or a URI that handles the callback from the authorization
+                     server.
+
+    If oauth_callback is 'oob' then pass in the
+    generated verification code to step2_exchange,
+    otherwise pass in the query parameters received
+    at the callback uri to step2_exchange.
     """
     consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
     client = oauth.Client(consumer)
@@ -93,10 +194,11 @@
     }
     body = urllib.urlencode({'oauth_callback': oauth_callback})
     uri = _oauth_uri('request', self.discovery, self.params)
+
     resp, content = client.request(uri, 'POST', headers=headers,
                                    body=body)
     if resp['status'] != '200':
-      print content
+      logging.error('Failed to retrieve temporary authorization: %s' % content)
       raise Exception('Invalid response %s.' % resp['status'])
 
     self.request_token = dict(parse_qsl(content))
@@ -104,15 +206,24 @@
     auth_params = copy.copy(self.params)
     auth_params['oauth_token'] = self.request_token['oauth_token']
 
-    uri = _oauth_uri('authorize', self.discovery, auth_params)
-    return uri
+    return _oauth_uri('authorize', self.discovery, auth_params)
 
-  def step2_pin(self, pin):
-    """Returns an oauth_token and oauth_token_secret in a dictionary"""
+  def step2_exchange(self, verifier):
+    """Exhanges an authorized request token
+    for OAuthCredentials.
 
-    token = oauth.Token(self.request_token['oauth_token'],
+    verifier - either the verifier token, or a dictionary
+        of the query parameters to the callback, which contains
+        the oauth_verifier.
+    """
+
+    if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
+      verifier = verifier['oauth_verifier']
+
+    token = oauth.Token(
+        self.request_token['oauth_token'],
         self.request_token['oauth_token_secret'])
-    token.set_verifier(pin)
+    token.set_verifier(verifier)
     consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
     client = oauth.Client(consumer, token)
 
@@ -123,8 +234,13 @@
 
     uri = _oauth_uri('access', self.discovery, self.params)
     resp, content = client.request(uri, 'POST', headers=headers)
-    return dict(parse_qsl(content))
+    if resp['status'] != '200':
+      logging.error('Failed to retrieve access token: %s' % content)
+      raise Exception('Invalid response %s.' % resp['status'])
 
-  def step2_callback(self, query_params):
-    """Returns an access token via oauth.Token"""
-    pass
+    oauth_params = dict(parse_qsl(content))
+    token = oauth.Token(
+        oauth_params['oauth_token'],
+        oauth_params['oauth_token_secret'])
+
+    return OAuthCredentials(consumer, token, self.user_agent)