Starting to cleanup, organize files, and make it look like a real project.
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..d969683
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,6 @@
+syntax: glob
+
+*.pyc
+.*.swp
+*/.git/*
+.gitignore
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..838feef
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,2 @@
+default:
+	python discovery.py
diff --git a/README b/README
new file mode 100644
index 0000000..ef2c067
--- /dev/null
+++ b/README
@@ -0,0 +1,35 @@
+This is a prototype implementation of a client
+for discovery based APIs.
+
+Installation
+============
+
+None. 
+
+For the time being the required libraries
+are checked into this directory to make 
+developement easier, so you can just run
+this directly after checking out.
+
+Running
+=======
+
+First run three-legged-dance.py to get OAuth
+tokens for Buzz, which will be stored in a file.
+
+   $ python three-legged-dance.py
+
+Then run sample.py, which will use the apiclient
+library to retrieve the title of the most
+recent entry in Buzz.
+
+   $ python sample.py
+
+
+Third Pary Libraries
+====================
+
+http://code.google.com/p/httplib2
+http://code.google.com/p/uri-templates
+http://github.com/simplegeo/python-oauth2
+
diff --git a/apiclient/__init__.py b/apiclient/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apiclient/__init__.py
diff --git a/apiclient/discovery.py b/apiclient/discovery.py
new file mode 100644
index 0000000..85d2049
--- /dev/null
+++ b/apiclient/discovery.py
@@ -0,0 +1,176 @@
+# 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.
+
+"""Client for discovery based APIs
+
+A client library for Google's discovery
+based APIs.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+import httplib2
+import re
+import simplejson
+import urlparse
+import uritemplate
+
+class HttpError(Exception): pass
+
+DISCOVERY_URI = 'http://www.googleapis.com/discovery/0.1/describe{?api,apiVersion}'
+
+
+def key2method(key):
+  """
+  max-results -> MaxResults
+  """
+  result = []
+  key = list(key)
+  newWord = True
+  if not key[0].isalpha():
+    result.append('X')
+    newWord = False
+  for c in key:
+    if c.isalnum():
+      if newWord:
+        result.append(c.upper())
+        newWord = False
+      else:
+        result.append(c.lower())
+    else:
+      newWord = True
+
+  return ''.join(result)
+
+
+def key2param(key):
+  """
+  max-results -> max_results
+  """
+  result = []
+  key = list(key)
+  if not key[0].isalpha():
+    result.append('x')
+  for c in key:
+    if c.isalnum():
+      result.append(c)
+    else:
+      result.append('_')
+
+  return ''.join(result)
+
+
+class JsonModel(object):
+  def request(self, headers, params):
+    model = params.get('body', None)
+    query = '?alt=json&prettyprint=true'
+    headers['Accept'] = 'application/json'
+    if model == None:
+      return (headers, params, query, None)
+    else:
+      model = {'data': model }
+      headers['Content-Type'] = 'application/json'
+      del params['body']
+      return (headers, params, query, simplejson.dumps(model))
+
+  def response(self, resp, content):
+    # Error handling is TBD
+    if resp.status < 300:
+      return simplejson.loads(content)['data']
+    else:
+      if resp['content-type'] != 'application/json':
+        raise HttpError("%d %s" % (resp.status, resp.reason))
+      else:
+        raise HttpError(simplejson.loads(content)['error'])
+
+
+def build(service, version, http=httplib2.Http(),
+    discoveryServiceUrl = DISCOVERY_URI, auth = None, model = JsonModel()):
+  params = {
+      'api': service,
+      'apiVersion': version
+      }
+  resp, content = http.request(uritemplate.expand(discoveryServiceUrl, params))
+  d = simplejson.loads(content)
+  service = d['data'][service][version]
+  base = service['baseUrl']
+  resources = service['resources']
+
+  class Service(object):
+    """Top level interface for a service"""
+
+    def __init__(self, http=http):
+      self._http = http
+      self._baseUrl = base
+      self._model = model
+
+  def createMethod(theclass, methodName, methodDesc):
+    def method(self, **kwargs):
+      return createResource(self._http, self._baseUrl, self._model,
+          methodName, methodDesc)
+
+    setattr(method, '__doc__', 'A description of how to use this function')
+    setattr(theclass, methodName, method)
+
+  for methodName, methodDesc in resources.iteritems():
+    createMethod(Service, methodName, methodDesc)
+  return Service()
+
+
+def createResource(http, baseUrl, model, resourceName, resourceDesc):
+
+  class Resource(object):
+    """A class for interacting with a resource."""
+
+    def __init__(self):
+      self._http = http
+      self._baseUrl = baseUrl
+      self._model = model
+
+  def createMethod(theclass, methodName, methodDesc):
+    pathUrl = methodDesc['pathUrl']
+    pathUrl = re.sub(r'\{', r'{+', pathUrl)
+    httpMethod = methodDesc['httpMethod']
+    args = methodDesc['parameters'].keys()
+    if httpMethod in ['PUT', 'POST']:
+      args.append('body')
+    argmap = dict([(key2param(key), key) for key in args])
+
+    def method(self, **kwargs):
+      for name in kwargs.iterkeys():
+        if name not in argmap:
+          raise TypeError('Got an unexpected keyword argument "%s"' % name)
+      params = dict(
+          [(argmap[key], value) for key, value in kwargs.iteritems()]
+          )
+      headers = {}
+      headers, params, query, body = self._model.request(headers, params)
+
+      url = urlparse.urljoin(self._baseUrl,
+          uritemplate.expand(pathUrl, params) + query)
+      return self._model.response(*self._http.request(
+        url, method=httpMethod, headers=headers, body=body))
+
+    docs = ['A description of how to use this function\n\n']
+    for arg in argmap.iterkeys():
+      docs.append('%s - A parameter\n' % arg)
+
+    setattr(method, '__doc__', ''.join(docs))
+    setattr(theclass, methodName, method)
+
+  for methodName, methodDesc in resourceDesc['methods'].iteritems():
+    createMethod(Resource, methodName, methodDesc)
+
+  return Resource()
diff --git a/discovery.json b/discovery.json
deleted file mode 100644
index 0ab65f4..0000000
--- a/discovery.json
+++ /dev/null
@@ -1,396 +0,0 @@
-{
- "data": {
-  "buzz": {
-   "0.1": {
-    "baseUrl": "https://apiary-test.corp.google.com/",
-    "resources": {
-     "feeds": {
-      "methods": {
-       "list": {
-        "pathUrl": "buzz/v1/feeds/{userId}/{scope}",
-        "rpcName": "chili.feeds.list",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "scope": {
-          "parameterType": "path",
-          "pattern": "@.*"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "delete": {
-        "pathUrl": "buzz/v1/feeds/{userId}/@self/{siteId}",
-        "rpcName": "chili.feeds.delete",
-        "httpMethod": "DELETE",
-        "methodType": "rest",
-        "parameters": {
-         "siteId": {
-          "parameterType": "path"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "insert": {
-        "pathUrl": "buzz/v1/feeds/{userId}/@self",
-        "rpcName": "chili.feeds.insert",
-        "httpMethod": "POST",
-        "methodType": "rest",
-        "parameters": {
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "update": {
-        "pathUrl": "buzz/v1/feeds/{userId}/@self/{siteId}",
-        "rpcName": "chili.feeds.update",
-        "httpMethod": "PUT",
-        "methodType": "rest",
-        "parameters": {
-         "siteId": {
-          "parameterType": "path"
-         },
-         "max-results": {
-          "parameterType": "query"
-         },
-         "c": {
-          "parameterType": "query"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       }
-      }
-     },
-     "activities": {
-      "methods": {
-       "update": {
-        "pathUrl": "buzz/v1/activities/{userId}/{scope}/{postId}",
-        "rpcName": "chili.activities.update",
-        "httpMethod": "PUT",
-        "methodType": "rest",
-        "parameters": {
-         "scope": {
-          "parameterType": "path",
-          "pattern": "@.*"
-         },
-         "userId": {
-          "parameterType": "path"
-         },
-         "postId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "delete": {
-        "pathUrl": "buzz/v1/activities/{userId}/{scope}/{postId}",
-        "rpcName": "chili.activities.delete",
-        "httpMethod": "DELETE",
-        "methodType": "rest",
-        "parameters": {
-         "scope": {
-          "parameterType": "path",
-          "pattern": "@.*"
-         },
-         "userId": {
-          "parameterType": "path"
-         },
-         "postId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "list": {
-        "pathUrl": "buzz/v1/activities/{userId}/{scope}",
-        "rpcName": "chili.activities.list",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "scope": {
-          "parameterType": "path",
-          "pattern": "@.*"
-         },
-         "max-results": {
-          "parameterType": "query"
-         },
-         "c": {
-          "parameterType": "query"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "insert": {
-        "pathUrl": "buzz/v1/activities/{userId}/@self",
-        "rpcName": "chili.activities.insert",
-        "httpMethod": "POST",
-        "methodType": "rest",
-        "parameters": {
-         "preview": {
-          "parameterType": "query"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "get": {
-        "pathUrl": "buzz/v1/activities/{userId}/@self/{postId}",
-        "rpcName": "chili.activities.get",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "userId": {
-          "parameterType": "path"
-         },
-         "postId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "search": {
-        "pathUrl": "buzz/v1/activities/search",
-        "rpcName": "chili.activities.search",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "geocode": {
-          "parameterType": "query"
-         },
-         "max-results": {
-          "parameterType": "query"
-         },
-         "c": {
-          "parameterType": "query"
-         },
-         "q": {
-          "parameterType": "query"
-         }
-        }
-       }
-      }
-     },
-     "people": {
-      "methods": {
-       "list": {
-        "pathUrl": "buzz/v1/people/{userId}/{scope}",
-        "rpcName": "chili.people.list",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "scope": {
-          "parameterType": "path",
-          "pattern": "@.*"
-         },
-         "max-results": {
-          "parameterType": "query"
-         },
-         "c": {
-          "parameterType": "query"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "delete": {
-        "pathUrl": "buzz/v1/people/{userId}/{scope}/{personId}",
-        "rpcName": "chili.people.delete",
-        "httpMethod": "DELETE",
-        "methodType": "rest",
-        "parameters": {
-         "scope": {
-          "parameterType": "path",
-          "pattern": "@.*"
-         },
-         "userId": {
-          "parameterType": "path"
-         },
-         "personId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "get": {
-        "pathUrl": "buzz/v1/people/{userId}/@self",
-        "rpcName": "chili.people.get",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "update": {
-        "pathUrl": "buzz/v1/people/{userId/{scope}/{personId}",
-        "rpcName": "chili.people.update",
-        "httpMethod": "PUT",
-        "methodType": "rest",
-        "parameters": {
-         "scope": {
-          "parameterType": "query"
-         },
-         "userId": {
-          "parameterType": "query"
-         },
-         "personId": {
-          "parameterType": "path"
-         }
-        }
-       }
-      }
-     },
-     "groups": {
-      "methods": {
-       "get": {
-        "pathUrl": "buzz/v1/groups/{userId}/{groupId}",
-        "rpcName": "chili.groups.get",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "groupId": {
-          "parameterType": "path"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "list": {
-        "pathUrl": "buzz/v1/groups/{userId}",
-        "rpcName": "chili.groups.list",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "max-results": {
-          "parameterType": "query"
-         },
-         "c": {
-          "parameterType": "query"
-         },
-         "userId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "delete": {
-        "pathUrl": "buzz/v1/groups/{userId}/{groupId}/{personId}",
-        "rpcName": "chili.groups.delete",
-        "httpMethod": "DELETE",
-        "methodType": "rest",
-        "parameters": {
-         "groupId": {
-          "parameterType": "path"
-         },
-         "userId": {
-          "parameterType": "path"
-         },
-         "personId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "update": {
-        "pathUrl": "buzz/v1/groups/{userId}/{groupId}/{personId}",
-        "rpcName": "chili.groups.update",
-        "httpMethod": "PUT",
-        "methodType": "rest",
-        "parameters": {
-         "groupId": {
-          "parameterType": "path"
-         },
-         "userId": {
-          "parameterType": "path"
-         },
-         "personId": {
-          "parameterType": "path"
-         }
-        }
-       }
-      }
-     },
-     "comments": {
-      "methods": {
-       "update": {
-        "pathUrl": "buzz/v1/activities/{userId}/@self/{postId}/@comments/{commentId}",
-        "rpcName": "chili.comments.update",
-        "httpMethod": "PUT",
-        "methodType": "rest",
-        "parameters": {
-         "userId": {
-          "parameterType": "path"
-         },
-         "commentId": {
-          "parameterType": "path"
-         },
-         "postId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "insert": {
-        "pathUrl": "buzz/v1/activities/{userId}/@self/{postId}/@comments",
-        "rpcName": "chili.comments.insert",
-        "httpMethod": "POST",
-        "methodType": "rest",
-        "parameters": {
-         "userId": {
-          "parameterType": "path"
-         },
-         "postId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "list": {
-        "pathUrl": "buzz/v1/activities/{userId}/@self/{postId}/@comments",
-        "rpcName": "chili.comments.list",
-        "httpMethod": "GET",
-        "methodType": "rest",
-        "parameters": {
-         "max-results": {
-          "parameterType": "query"
-         },
-         "c": {
-          "parameterType": "query"
-         },
-         "userId": {
-          "parameterType": "path"
-         },
-         "postId": {
-          "parameterType": "path"
-         }
-        }
-       },
-       "delete": {
-        "pathUrl": "buzz/v1/activities/{userId}/@self/{postId}/@comments/{commentId}",
-        "rpcName": "chili.comments.delete",
-        "httpMethod": "DELETE",
-        "methodType": "rest",
-        "parameters": {
-         "userId": {
-          "parameterType": "path"
-         },
-         "commentId": {
-          "parameterType": "path"
-         },
-         "postId": {
-          "parameterType": "path"
-         }
-        }
-       }
-      }
-     }
-    }
-   }
-  }
- }
-}
diff --git a/discovery.py b/discovery.py
deleted file mode 100644
index b9e7edf..0000000
--- a/discovery.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/python2.4
-#
-# Copyright 2010 Google Inc. All Rights Reserved.
-
-"""One-line documentation for discovery module.
-
-A detailed description of discovery.
-"""
-
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-import simplejson
-
-def discovery():
-  d = simplejson.load(open("discovery.json", "r"))
-  desc = d["data"]["buzz"]["0.1"]
-  base = desc["baseUrl"]
-  feeds = desc["resources"]["feeds"]
-  methods = feeds["methods"]
-  list_method = methods["list"]
-  print list_method
-
-  class Proto(object):
-    """A class for interacting with a service"""
-    pass
-
-  def doList(self, scope, userId):
-    print "Hello"
-
-  setattr(doList, "__doc__", "A description of how to use this function")
-  setattr(Proto, "list", doList)
-
-  return Proto()
-
-
-def main():
-  p = discovery()
-  p.list("foo", "bar")
-  print dir(p)
-  print help(p)
-
-
-if __name__ == '__main__':
-  main()
diff --git a/oauth2/__init__.py b/oauth2/__init__.py
new file mode 100644
index 0000000..3adbd20
--- /dev/null
+++ b/oauth2/__init__.py
@@ -0,0 +1,735 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+import httplib2
+
+try:
+    from urlparse import parse_qs, parse_qsl
+except ImportError:
+    from cgi import parse_qs, parse_qsl
+
+
+VERSION = '1.0'  # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class Error(RuntimeError):
+    """Generic exception class."""
+
+    def __init__(self, message='OAuth error occurred.'):
+        self._message = message
+
+    @property
+    def message(self):
+        """A hack to get around the deprecation errors in 2.6."""
+        return self._message
+
+    def __str__(self):
+        return self._message
+
+
+class MissingSignature(Error):
+    pass
+
+
+def build_authenticate_header(realm=''):
+    """Optional WWW-Authenticate header (401 error)"""
+    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+
+def build_xoauth_string(url, consumer, token=None):
+    """Build an XOAUTH string for use in SMTP/IMPA authentication."""
+    request = Request.from_consumer_and_token(consumer, token,
+        "GET", url)
+
+    signing_method = SignatureMethod_HMAC_SHA1()
+    request.sign_request(signing_method, consumer, token)
+
+    params = []
+    for k, v in sorted(request.iteritems()):
+        if v is not None:
+            params.append('%s="%s"' % (k, escape(v)))
+
+    return "%s %s %s" % ("GET", url, ','.join(params))
+
+
+def escape(s):
+    """Escape a URL including any /."""
+    return urllib.quote(s, safe='~')
+
+
+def generate_timestamp():
+    """Get seconds since epoch (UTC)."""
+    return int(time.time())
+
+
+def generate_nonce(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+def generate_verifier(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class Consumer(object):
+    """A consumer of OAuth-protected services.
+ 
+    The OAuth consumer is a "third-party" service that wants to access
+    protected resources from an OAuth service provider on behalf of an end
+    user. It's kind of the OAuth client.
+ 
+    Usually a consumer must be registered with the service provider by the
+    developer of the consumer software. As part of that process, the service
+    provider gives the consumer a *key* and a *secret* with which the consumer
+    software can identify itself to the service. The consumer will include its
+    key in each request to identify itself, but will use its secret only when
+    signing requests, to prove that the request is from that particular
+    registered consumer.
+ 
+    Once registered, the consumer can then use its consumer credentials to ask
+    the service provider for a request token, kicking off the OAuth
+    authorization process.
+    """
+
+    key = None
+    secret = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+        if self.key is None or self.secret is None:
+            raise ValueError("Key and secret must be set.")
+
+    def __str__(self):
+        data = {'oauth_consumer_key': self.key,
+            'oauth_consumer_secret': self.secret}
+
+        return urllib.urlencode(data)
+
+
+class Token(object):
+    """An OAuth credential used to request authorization or a protected
+    resource.
+ 
+    Tokens in OAuth comprise a *key* and a *secret*. The key is included in
+    requests to identify the token being used, but the secret is used only in
+    the signature, to prove that the requester is who the server gave the
+    token to.
+ 
+    When first negotiating the authorization, the consumer asks for a *request
+    token* that the live user authorizes with the service provider. The
+    consumer then exchanges the request token for an *access token* that can
+    be used to access protected resources.
+    """
+
+    key = None
+    secret = None
+    callback = None
+    callback_confirmed = None
+    verifier = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+        if self.key is None or self.secret is None:
+            raise ValueError("Key and secret must be set.")
+
+    def set_callback(self, callback):
+        self.callback = callback
+        self.callback_confirmed = 'true'
+
+    def set_verifier(self, verifier=None):
+        if verifier is not None:
+            self.verifier = verifier
+        else:
+            self.verifier = generate_verifier()
+
+    def get_callback_url(self):
+        if self.callback and self.verifier:
+            # Append the oauth_verifier.
+            parts = urlparse.urlparse(self.callback)
+            scheme, netloc, path, params, query, fragment = parts[:6]
+            if query:
+                query = '%s&oauth_verifier=%s' % (query, self.verifier)
+            else:
+                query = 'oauth_verifier=%s' % self.verifier
+            return urlparse.urlunparse((scheme, netloc, path, params,
+                query, fragment))
+        return self.callback
+
+    def to_string(self):
+        """Returns this token as a plain string, suitable for storage.
+ 
+        The resulting string includes the token's secret, so you should never
+        send or store this string where a third party can read it.
+        """
+
+        data = {
+            'oauth_token': self.key,
+            'oauth_token_secret': self.secret,
+        }
+
+        if self.callback_confirmed is not None:
+            data['oauth_callback_confirmed'] = self.callback_confirmed
+        return urllib.urlencode(data)
+ 
+    @staticmethod
+    def from_string(s):
+        """Deserializes a token from a string like one returned by
+        `to_string()`."""
+
+        if not len(s):
+            raise ValueError("Invalid parameter string.")
+
+        params = parse_qs(s, keep_blank_values=False)
+        if not len(params):
+            raise ValueError("Invalid parameter string.")
+
+        try:
+            key = params['oauth_token'][0]
+        except Exception:
+            raise ValueError("'oauth_token' not found in OAuth request.")
+
+        try:
+            secret = params['oauth_token_secret'][0]
+        except Exception:
+            raise ValueError("'oauth_token_secret' not found in " 
+                "OAuth request.")
+
+        token = Token(key, secret)
+        try:
+            token.callback_confirmed = params['oauth_callback_confirmed'][0]
+        except KeyError:
+            pass  # 1.0, no callback confirmed.
+        return token
+
+    def __str__(self):
+        return self.to_string()
+
+
+def setter(attr):
+    name = attr.__name__
+ 
+    def getter(self):
+        try:
+            return self.__dict__[name]
+        except KeyError:
+            raise AttributeError(name)
+ 
+    def deleter(self):
+        del self.__dict__[name]
+ 
+    return property(getter, attr, deleter)
+
+
+class Request(dict):
+ 
+    """The parameters and information for an HTTP request, suitable for
+    authorizing with OAuth credentials.
+ 
+    When a consumer wants to access a service's protected resources, it does
+    so using a signed HTTP request identifying itself (the consumer) with its
+    key, and providing an access token authorized by the end user to access
+    those resources.
+ 
+    """
+ 
+    version = VERSION
+ 
+    def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
+        self.method = method
+        self.url = url
+        if parameters is not None:
+            self.update(parameters)
+ 
+    @setter
+    def url(self, value):
+        self.__dict__['url'] = value
+        if value is not None:
+            scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
+
+            # Exclude default port numbers.
+            if scheme == 'http' and netloc[-3:] == ':80':
+                netloc = netloc[:-3]
+            elif scheme == 'https' and netloc[-4:] == ':443':
+                netloc = netloc[:-4]
+            if scheme not in ('http', 'https'):
+                raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
+
+            # Normalized URL excludes params, query, and fragment.
+            self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
+        else:
+            self.normalized_url = None
+            self.__dict__['url'] = None
+ 
+    @setter
+    def method(self, value):
+        self.__dict__['method'] = value.upper()
+ 
+    def _get_timestamp_nonce(self):
+        return self['oauth_timestamp'], self['oauth_nonce']
+ 
+    def get_nonoauth_parameters(self):
+        """Get any non-OAuth parameters."""
+        return dict([(k, v) for k, v in self.iteritems() 
+                    if not k.startswith('oauth_')])
+ 
+    def to_header(self, realm=''):
+        """Serialize as a header for an HTTPAuth request."""
+        oauth_params = ((k, v) for k, v in self.items() 
+                            if k.startswith('oauth_'))
+        stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
+        header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
+        params_header = ', '.join(header_params)
+ 
+        auth_header = 'OAuth realm="%s"' % realm
+        if params_header:
+            auth_header = "%s, %s" % (auth_header, params_header)
+ 
+        return {'Authorization': auth_header}
+ 
+    def to_postdata(self):
+        """Serialize as post data for a POST request."""
+        # tell urlencode to deal with sequence values and map them correctly
+        # to resulting querystring. for example self["k"] = ["v1", "v2"] will
+        # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
+        return urllib.urlencode(self, True)
+ 
+    def to_url(self):
+        """Serialize as a URL for a GET request."""
+        base_url = urlparse.urlparse(self.url)
+        query = parse_qs(base_url.query)
+        for k, v in self.items():
+            query.setdefault(k, []).append(v)
+        url = (base_url.scheme, base_url.netloc, base_url.path, base_url.params,
+               urllib.urlencode(query, True), base_url.fragment)
+        return urlparse.urlunparse(url)
+
+    def get_parameter(self, parameter):
+        ret = self.get(parameter)
+        if ret is None:
+            raise Error('Parameter not found: %s' % parameter)
+
+        return ret
+ 
+    def get_normalized_parameters(self):
+        """Return a string that contains the parameters that must be signed."""
+        items = []
+        for key, value in self.iteritems():
+            if key == 'oauth_signature':
+                continue
+            # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
+            # so we unpack sequence values into multiple items for sorting.
+            if hasattr(value, '__iter__'):
+                items.extend((key, item) for item in value)
+            else:
+                items.append((key, value))
+
+        # Include any query string parameters from the provided URL
+        query = urlparse.urlparse(self.url)[4]
+        items.extend(self._split_url_string(query).items())
+
+        encoded_str = urllib.urlencode(sorted(items))
+        # Encode signature parameters per Oauth Core 1.0 protocol
+        # spec draft 7, section 3.6
+        # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
+        # Spaces must be encoded with "%20" instead of "+"
+        return encoded_str.replace('+', '%20')
+ 
+    def sign_request(self, signature_method, consumer, token):
+        """Set the signature parameter to the result of sign."""
+
+        if 'oauth_consumer_key' not in self:
+            self['oauth_consumer_key'] = consumer.key
+
+        if token and 'oauth_token' not in self:
+            self['oauth_token'] = token.key
+
+        self['oauth_signature_method'] = signature_method.name
+        self['oauth_signature'] = signature_method.sign(self, consumer, token)
+ 
+    @classmethod
+    def make_timestamp(cls):
+        """Get seconds since epoch (UTC)."""
+        return str(int(time.time()))
+ 
+    @classmethod
+    def make_nonce(cls):
+        """Generate pseudorandom number."""
+        return str(random.randint(0, 100000000))
+ 
+    @classmethod
+    def from_request(cls, http_method, http_url, headers=None, parameters=None,
+            query_string=None):
+        """Combines multiple parameter sources."""
+        if parameters is None:
+            parameters = {}
+ 
+        # Headers
+        if headers and 'Authorization' in headers:
+            auth_header = headers['Authorization']
+            # Check that the authorization header is OAuth.
+            if auth_header[:6] == 'OAuth ':
+                auth_header = auth_header[6:]
+                try:
+                    # Get the parameters from the header.
+                    header_params = cls._split_header(auth_header)
+                    parameters.update(header_params)
+                except:
+                    raise Error('Unable to parse OAuth parameters from '
+                        'Authorization header.')
+ 
+        # GET or POST query string.
+        if query_string:
+            query_params = cls._split_url_string(query_string)
+            parameters.update(query_params)
+ 
+        # URL parameters.
+        param_str = urlparse.urlparse(http_url)[4] # query
+        url_params = cls._split_url_string(param_str)
+        parameters.update(url_params)
+ 
+        if parameters:
+            return cls(http_method, http_url, parameters)
+ 
+        return None
+ 
+    @classmethod
+    def from_consumer_and_token(cls, consumer, token=None,
+            http_method=HTTP_METHOD, http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+ 
+        defaults = {
+            'oauth_consumer_key': consumer.key,
+            'oauth_timestamp': cls.make_timestamp(),
+            'oauth_nonce': cls.make_nonce(),
+            'oauth_version': cls.version,
+        }
+ 
+        defaults.update(parameters)
+        parameters = defaults
+ 
+        if token:
+            parameters['oauth_token'] = token.key
+            if token.verifier:
+                parameters['oauth_verifier'] = token.verifier
+ 
+        return Request(http_method, http_url, parameters)
+ 
+    @classmethod
+    def from_token_and_callback(cls, token, callback=None, 
+        http_method=HTTP_METHOD, http_url=None, parameters=None):
+
+        if not parameters:
+            parameters = {}
+ 
+        parameters['oauth_token'] = token.key
+ 
+        if callback:
+            parameters['oauth_callback'] = callback
+ 
+        return cls(http_method, http_url, parameters)
+ 
+    @staticmethod
+    def _split_header(header):
+        """Turn Authorization: header into parameters."""
+        params = {}
+        parts = header.split(',')
+        for param in parts:
+            # Ignore realm parameter.
+            if param.find('realm') > -1:
+                continue
+            # Remove whitespace.
+            param = param.strip()
+            # Split key-value.
+            param_parts = param.split('=', 1)
+            # Remove quotes and unescape the value.
+            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+        return params
+ 
+    @staticmethod
+    def _split_url_string(param_str):
+        """Turn URL string into parameters."""
+        parameters = parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.iteritems():
+            parameters[k] = urllib.unquote(v[0])
+        return parameters
+
+
+class Client(httplib2.Http):
+    """OAuthClient is a worker to attempt to execute a request."""
+
+    def __init__(self, consumer, token=None, cache=None, timeout=None,
+        proxy_info=None):
+
+        if consumer is not None and not isinstance(consumer, Consumer):
+            raise ValueError("Invalid consumer.")
+
+        if token is not None and not isinstance(token, Token):
+            raise ValueError("Invalid token.")
+
+        self.consumer = consumer
+        self.token = token
+        self.method = SignatureMethod_HMAC_SHA1()
+
+        httplib2.Http.__init__(self, cache=cache, timeout=timeout, 
+            proxy_info=proxy_info)
+
+    def set_signature_method(self, method):
+        if not isinstance(method, SignatureMethod):
+            raise ValueError("Invalid signature method.")
+
+        self.method = method
+
+    def request(self, uri, method="GET", body=None, headers=None, 
+        redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
+        DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'
+
+        if not isinstance(headers, dict):
+            headers = {}
+
+        is_multipart = method == 'POST' and headers.get('Content-Type', 
+            DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE
+
+        if body and method == "POST" and not is_multipart:
+            parameters = dict(parse_qsl(body))
+        else:
+            parameters = None
+
+        req = Request.from_consumer_and_token(self.consumer, 
+            token=self.token, http_method=method, http_url=uri, 
+            parameters=parameters)
+
+        req.sign_request(self.method, self.consumer, self.token)
+
+        if method == "POST":
+            headers['Content-Type'] = headers.get('Content-Type', 
+                DEFAULT_CONTENT_TYPE)
+            if is_multipart:
+                headers.update(req.to_header())
+            else:
+                body = req.to_postdata()
+        elif method == "GET":
+            uri = req.to_url()
+        else:
+            headers.update(req.to_header())
+
+        return httplib2.Http.request(self, uri, method=method, body=body, 
+            headers=headers, redirections=redirections, 
+            connection_type=connection_type)
+
+
+class Server(object):
+    """A skeletal implementation of a service provider, providing protected
+    resources to requests from authorized consumers.
+ 
+    This class implements the logic to check requests for authorization. You
+    can use it with your web server or web framework to protect certain
+    resources with OAuth.
+    """
+
+    timestamp_threshold = 300 # In seconds, five minutes.
+    version = VERSION
+    signature_methods = None
+
+    def __init__(self, signature_methods=None):
+        self.signature_methods = signature_methods or {}
+
+    def add_signature_method(self, signature_method):
+        self.signature_methods[signature_method.name] = signature_method
+        return self.signature_methods
+
+    def verify_request(self, request, consumer, token):
+        """Verifies an api call and checks all the parameters."""
+
+        version = self._get_version(request)
+        self._check_signature(request, consumer, token)
+        parameters = request.get_nonoauth_parameters()
+        return parameters
+
+    def build_authenticate_header(self, realm=''):
+        """Optional support for the authenticate header."""
+        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+    def _get_version(self, request):
+        """Verify the correct version request for this server."""
+        try:
+            version = request.get_parameter('oauth_version')
+        except:
+            version = VERSION
+
+        if version and version != self.version:
+            raise Error('OAuth version %s not supported.' % str(version))
+
+        return version
+
+    def _get_signature_method(self, request):
+        """Figure out the signature with some defaults."""
+        try:
+            signature_method = request.get_parameter('oauth_signature_method')
+        except:
+            signature_method = SIGNATURE_METHOD
+
+        try:
+            # Get the signature method object.
+            signature_method = self.signature_methods[signature_method]
+        except:
+            signature_method_names = ', '.join(self.signature_methods.keys())
+            raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
+
+        return signature_method
+
+    def _get_verifier(self, request):
+        return request.get_parameter('oauth_verifier')
+
+    def _check_signature(self, request, consumer, token):
+        timestamp, nonce = request._get_timestamp_nonce()
+        self._check_timestamp(timestamp)
+        signature_method = self._get_signature_method(request)
+
+        try:
+            signature = request.get_parameter('oauth_signature')
+        except:
+            raise MissingSignature('Missing oauth_signature.')
+
+        # Validate the signature.
+        valid = signature_method.check(request, consumer, token, signature)
+
+        if not valid:
+            key, base = signature_method.signing_base(request, consumer, token)
+
+            raise Error('Invalid signature. Expected signature base ' 
+                'string: %s' % base)
+
+        built = signature_method.sign(request, consumer, token)
+
+    def _check_timestamp(self, timestamp):
+        """Verify that timestamp is recentish."""
+        timestamp = int(timestamp)
+        now = int(time.time())
+        lapsed = now - timestamp
+        if lapsed > self.timestamp_threshold:
+            raise Error('Expired timestamp: given %d and now %s has a '
+                'greater difference than threshold %d' % (timestamp, now, 
+                    self.timestamp_threshold))
+
+
+class SignatureMethod(object):
+    """A way of signing requests.
+ 
+    The OAuth protocol lets consumers and service providers pick a way to sign
+    requests. This interface shows the methods expected by the other `oauth`
+    modules for signing requests. Subclass it and implement its methods to
+    provide a new way to sign requests.
+    """
+
+    def signing_base(self, request, consumer, token):
+        """Calculates the string that needs to be signed.
+
+        This method returns a 2-tuple containing the starting key for the
+        signing and the message to be signed. The latter may be used in error
+        messages to help clients debug their software.
+
+        """
+        raise NotImplementedError
+
+    def sign(self, request, consumer, token):
+        """Returns the signature for the given request, based on the consumer
+        and token also provided.
+
+        You should use your implementation of `signing_base()` to build the
+        message to sign. Otherwise it may be less useful for debugging.
+
+        """
+        raise NotImplementedError
+
+    def check(self, request, consumer, token, signature):
+        """Returns whether the given signature is the correct signature for
+        the given consumer and token signing the given request."""
+        built = self.sign(request, consumer, token)
+        return built == signature
+
+
+class SignatureMethod_HMAC_SHA1(SignatureMethod):
+    name = 'HMAC-SHA1'
+        
+    def signing_base(self, request, consumer, token):
+        if request.normalized_url is None:
+            raise ValueError("Base URL for request is not set.")
+
+        sig = (
+            escape(request.method),
+            escape(request.normalized_url),
+            escape(request.get_normalized_parameters()),
+        )
+
+        key = '%s&' % escape(consumer.secret)
+        if token:
+            key += escape(token.secret)
+        raw = '&'.join(sig)
+        return key, raw
+
+    def sign(self, request, consumer, token):
+        """Builds the base signature string."""
+        key, raw = self.signing_base(request, consumer, token)
+
+        # HMAC object.
+        try:
+            from hashlib import sha1 as sha
+        except ImportError:
+            import sha # Deprecated
+
+        hashed = hmac.new(key, raw, sha)
+
+        # Calculate the digest base 64.
+        return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class SignatureMethod_PLAINTEXT(SignatureMethod):
+
+    name = 'PLAINTEXT'
+
+    def signing_base(self, request, consumer, token):
+        """Concatenates the consumer key and secret with the token's
+        secret."""
+        sig = '%s&' % escape(consumer.secret)
+        if token:
+            sig = sig + escape(token.secret)
+        return sig, sig
+
+    def sign(self, request, consumer, token):
+        key, raw = self.signing_base(request, consumer, token)
+        return raw
diff --git a/oauth2/clients/__init__.py b/oauth2/clients/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/oauth2/clients/__init__.py
diff --git a/oauth2/clients/imap.py b/oauth2/clients/imap.py
new file mode 100644
index 0000000..68b7cd8
--- /dev/null
+++ b/oauth2/clients/imap.py
@@ -0,0 +1,40 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import oauth2
+import imaplib
+
+
+class IMAP4_SSL(imaplib.IMAP4_SSL):
+    """IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH."""
+
+    def authenticate(self, url, consumer, token):
+        if consumer is not None and not isinstance(consumer, oauth2.Consumer):
+            raise ValueError("Invalid consumer.")
+
+        if token is not None and not isinstance(token, oauth2.Token):
+            raise ValueError("Invalid token.")
+
+        imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH',
+            lambda x: oauth2.build_xoauth_string(url, consumer, token))
diff --git a/oauth2/clients/smtp.py b/oauth2/clients/smtp.py
new file mode 100644
index 0000000..3e7bf0b
--- /dev/null
+++ b/oauth2/clients/smtp.py
@@ -0,0 +1,41 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import oauth2
+import smtplib
+import base64
+
+
+class SMTP(smtplib.SMTP):
+    """SMTP wrapper for smtplib.SMTP that implements XOAUTH."""
+
+    def authenticate(self, url, consumer, token):
+        if consumer is not None and not isinstance(consumer, oauth2.Consumer):
+            raise ValueError("Invalid consumer.")
+
+        if token is not None and not isinstance(token, oauth2.Token):
+            raise ValueError("Invalid token.")
+
+        self.docmd('AUTH', 'XOAUTH %s' % \
+            base64.b64encode(oauth2.build_xoauth_string(url, consumer, token)))
diff --git a/runtests.py b/runtests.py
new file mode 100644
index 0000000..bda6090
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+import glob, unittest, os, sys
+
+from trace import fullmodname
+try:
+    from tests.utils import cleanup
+except:
+    def cleanup():
+        pass
+
+sys.path.insert(0, os.getcwd())
+
+# find all of the test modules
+modules = map(fullmodname, glob.glob(os.path.join('tests', 'test_*.py')))
+print "Running the tests found in the following modules:"
+print modules
+
+# load all of the tests into a suite
+try:
+    suite = unittest.TestLoader().loadTestsFromNames(modules)
+except Exception, exception:
+    # attempt to produce a more specific message
+    for module in modules: 
+        __import__(module)
+    raise
+
+verbosity = 1
+if "-q" in sys.argv or '--quiet' in sys.argv:
+    verbosity = 0
+if "-v" in sys.argv or '--verbose' in sys.argv:
+    verbosity = 2
+
+# run test suite
+unittest.TextTestRunner(verbosity=verbosity).run(suite)
+
+cleanup()
+
diff --git a/samples/cmdline/buzz.py b/samples/cmdline/buzz.py
new file mode 100644
index 0000000..9a0a909
--- /dev/null
+++ b/samples/cmdline/buzz.py
@@ -0,0 +1,115 @@
+#!/usr/bin/python2.4
+# -*- coding: utf-8 -*-
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+
+"""One-line documentation for discovery module.
+
+A detailed description of discovery.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+# TODO
+# - Add normalize_ that converts max-results into MaxResults
+#
+# - Each 'resource' should be its own object accessible
+#   from the service object returned from discovery.
+#
+# - Methods can either execute immediately or return 
+#   RestRequest objects which can be batched. 
+#
+# - 'Body' parameter for non-GET requests 
+#
+# - 2.x and 3.x compatible
+
+# JS also has the idea of a TransportRequest and a Transport.
+# The Transport has a doRequest() method that takes a request
+# and a callback function.
+# 
+
+
+# Discovery doc notes
+# - Which parameters are optional vs mandatory
+# - Is pattern a regex?
+# - Inconsistent naming max-results vs userId
+
+
+from apiclient.discovery import build
+
+import httplib2
+import simplejson
+import re
+
+import oauth2 as oauth
+
+def oauth_wrap(consumer, token, 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 = oauth_wrap(h)
+
+    Grumble. 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()
+
+    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(
+          consumer, token, http_method=method, http_url=uri)
+      req.sign_request(signer, consumer, token)
+      if headers == None:
+        headers = {}
+      headers.update(req.to_header())
+      headers['user-agent'] = 'jcgregorio-test-client'
+      return request_orig(uri, method, body, headers, redirections, connection_type)
+
+    http.request = new_request
+    return http
+
+def get_wrapped_http():
+  f = open("oauth_token.dat", "r")
+  oauth_params = simplejson.loads(f.read())
+
+  consumer = oauth.Consumer(oauth_params['consumer_key'], oauth_params['consumer_secret'])
+  token = oauth.Token(oauth_params['oauth_token'], oauth_params['oauth_token_secret'])
+
+  # Create a simple monkeypatch for httplib2.Http.request 
+  # just adds in the oauth authorization header and then calls
+  # the original request().
+  http = httplib2.Http()
+  return oauth_wrap(consumer, token, http)
+
+
+def main():
+  http = get_wrapped_http()
+  p = build("buzz", "v1", http = http)
+  activities = p.activities()
+  activitylist = activities.list(scope='@self', userId='@me')
+  print activitylist['items'][0]['title']
+  activities.insert(userId='@me', body={
+    'title': 'Testing insert',
+    'object': {
+      'content': u'Just a short note to show that insert is working. ☄', 
+      'type': 'note'}
+    }
+  )
+
+if __name__ == '__main__':
+  main()
diff --git a/samples/cmdline/oauth_token.dat b/samples/cmdline/oauth_token.dat
new file mode 100644
index 0000000..147863c
--- /dev/null
+++ b/samples/cmdline/oauth_token.dat
@@ -0,0 +1 @@
+{"consumer_secret": "anonymous", "oauth_token_secret": "R/eOG4BzDwvlXH+NwBuUzxFz", "consumer_key": "anonymous", "oauth_token": "1/41pApdDCG0cySvfZVhA5pT4I1F7cKRKR0P6Mb1Ik08o"}
\ No newline at end of file
diff --git a/samples/cmdline/three_legged_dance.py b/samples/cmdline/three_legged_dance.py
new file mode 100644
index 0000000..7219216
--- /dev/null
+++ b/samples/cmdline/three_legged_dance.py
@@ -0,0 +1,97 @@
+import urlparse
+import oauth2 as oauth
+import httplib2
+import urllib
+import simplejson
+
+try:
+    from urlparse import parse_qs, parse_qsl
+except ImportError:
+    from cgi import parse_qs, parse_qsl
+
+httplib2.debuglevel=4
+headers = {"user-agent": "jcgregorio-buzz-client",
+    'content-type': 'application/x-www-form-urlencoded'
+    }
+
+consumer_key = 'anonymous'
+consumer_secret = 'anonymous'
+
+request_token_url = 'https://www.google.com/accounts/OAuthGetRequestToken?domain=anonymous&scope=https://www.googleapis.com/auth/buzz'
+access_token_url = 'https://www.google.com/accounts/OAuthGetAccessToken?domain=anonymous&scope=https://www.googleapis.com/auth/buzz'
+authorize_url = 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?domain=anonymous&scope=https://www.googleapis.com/auth/buzz'
+
+consumer = oauth.Consumer(consumer_key, consumer_secret)
+client = oauth.Client(consumer)
+
+# Step 1: Get a request token. This is a temporary token that is used for 
+# having the user authorize an access token and to sign the request to obtain 
+# said access token.
+
+resp, content = client.request(request_token_url, "POST", headers=headers, body="oauth_callback=oob")
+if resp['status'] != '200':
+  print content
+  raise Exception("Invalid response %s." % resp['status'])
+
+request_token = dict(parse_qsl(content))
+
+print "Request Token:"
+print "    - oauth_token        = %s" % request_token['oauth_token']
+print "    - oauth_token_secret = %s" % request_token['oauth_token_secret']
+print 
+
+# Step 2: Redirect to the provider. Since this is a CLI script we do not 
+# redirect. In a web application you would redirect the user to the URL
+# below.
+
+base_url = urlparse.urlparse(authorize_url)
+query = parse_qs(base_url.query)
+query['oauth_token'] = request_token['oauth_token']
+
+print urllib.urlencode(query, True)
+
+url = (base_url.scheme, base_url.netloc, base_url.path, base_url.params,
+       urllib.urlencode(query, True), base_url.fragment)
+authorize_url = urlparse.urlunparse(url)
+
+print "Go to the following link in your browser:"
+print authorize_url
+print 
+
+# After the user has granted access to you, the consumer, the provider will
+# redirect you to whatever URL you have told them to redirect to. You can 
+# usually define this in the oauth_callback argument as well.
+accepted = 'n'
+while accepted.lower() == 'n':
+    accepted = raw_input('Have you authorized me? (y/n) ')
+oauth_verifier = raw_input('What is the PIN? ')
+
+# Step 3: Once the consumer has redirected the user back to the oauth_callback
+# URL you can request the access token the user has approved. You use the 
+# request token to sign this request. After this is done you throw away the
+# request token and use the access token returned. You should store this 
+# access token somewhere safe, like a database, for future use.
+token = oauth.Token(request_token['oauth_token'],
+    request_token['oauth_token_secret'])
+token.set_verifier(oauth_verifier)
+client = oauth.Client(consumer, token)
+
+resp, content = client.request(access_token_url, "POST", headers=headers)
+access_token = dict(parse_qsl(content))
+
+print "Access Token:"
+print "    - oauth_token        = %s" % access_token['oauth_token']
+print "    - oauth_token_secret = %s" % access_token['oauth_token_secret']
+print
+print "You may now access protected resources using the access tokens above." 
+print
+
+d = dict(
+  consumer_key = 'anonymous',
+  consumer_secret = 'anonymous'
+    )
+d.update(access_token)
+
+f = open("oauth_token.dat", "w")
+f.write(simplejson.dumps(d))
+f.close()
diff --git a/setpath.sh b/setpath.sh
new file mode 100644
index 0000000..45826d6
--- /dev/null
+++ b/setpath.sh
@@ -0,0 +1 @@
+export PYTHONPATH=`pwd`:$PYTHONPATH
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..e13b99b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+from distutils.core import setup
+
+# First pass at a setup.py, in the long run we will
+# need two, one for a version of the library that just
+# includes apiclient, and another that also includes 
+# all of the dependencies.
+setup(name="google-api-python-client",
+      version="0.1",
+      description="Google API Client Library for Python",
+      author="Joe Gregorio",
+      author_email="jcgregorio@google.com",
+      url="http://code.google.com/p/google-api-python-client/",
+      py_modules = ['apiclient', 'oauth2', 'simplejson', 'uritemplate'],
+      license = "Apache 2.0",
+      keywords="google api client")
diff --git a/simplejson/__init__.pyc b/simplejson/__init__.pyc
deleted file mode 100644
index 7eec1be..0000000
--- a/simplejson/__init__.pyc
+++ /dev/null
Binary files differ
diff --git a/simplejson/decoder.pyc b/simplejson/decoder.pyc
deleted file mode 100644
index debfebe..0000000
--- a/simplejson/decoder.pyc
+++ /dev/null
Binary files differ
diff --git a/simplejson/encoder.pyc b/simplejson/encoder.pyc
deleted file mode 100644
index d226624..0000000
--- a/simplejson/encoder.pyc
+++ /dev/null
Binary files differ
diff --git a/simplejson/ordered_dict.pyc b/simplejson/ordered_dict.pyc
deleted file mode 100644
index 823fe63..0000000
--- a/simplejson/ordered_dict.pyc
+++ /dev/null
Binary files differ
diff --git a/simplejson/scanner.pyc b/simplejson/scanner.pyc
deleted file mode 100644
index dda13e0..0000000
--- a/simplejson/scanner.pyc
+++ /dev/null
Binary files differ
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/test_oauth.py b/tests/test_oauth.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_oauth.py
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_pagination.py
diff --git a/uritemplate/__init__.py b/uritemplate/__init__.py
new file mode 100644
index 0000000..b447447
--- /dev/null
+++ b/uritemplate/__init__.py
@@ -0,0 +1,147 @@
+# Early, and incomplete implementation of -04.
+#
+import re
+import urllib
+
+RESERVED = ":/?#[]@!$&'()*+,;="
+OPERATOR = "+./;?|!@"
+EXPLODE = "*+"
+MODIFIER = ":^"
+TEMPLATE = re.compile(r"{(?P<operator>[\+\./;\?|!@])?(?P<varlist>[^}]+)}", re.UNICODE)
+VAR = re.compile(r"^(?P<varname>[^=\+\*:\^]+)((?P<explode>[\+\*])|(?P<partial>[:\^]-?[0-9]+))?(=(?P<default>.*))?$", re.UNICODE)
+
+def _tostring(varname, value, explode, operator, safe=""):
+  if type(value) == type([]):
+    if explode == "+":
+      return ",".join([varname + "." + urllib.quote(x, safe) for x in value])
+    else:
+      return ",".join([urllib.quote(x, safe) for x in value])
+  if type(value) == type({}):
+    keys = value.keys()
+    keys.sort()
+    if explode == "+":
+      return ",".join([varname + "." + urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
+    else:
+      return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
+  else:
+    return urllib.quote(value, safe)
+
+
+def _tostring_path(varname, value, explode, operator, safe=""):
+  joiner = operator
+  if type(value) == type([]):
+    if explode == "+":
+      return joiner.join([varname + "." + urllib.quote(x, safe) for x in value])
+    elif explode == "*":
+      return joiner.join([urllib.quote(x, safe) for x in value])
+    else:
+      return ",".join([urllib.quote(x, safe) for x in value])
+  elif type(value) == type({}):
+    keys = value.keys()
+    keys.sort()
+    if explode == "+":
+      return joiner.join([varname + "." + urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys])
+    elif explode == "*":
+      return joiner.join([urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys])
+    else:
+      return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
+  else:
+    if value:
+      return urllib.quote(value, safe)
+    else:
+      return ""
+
+def _tostring_query(varname, value, explode, operator, safe=""):
+  joiner = operator
+  varprefix = ""
+  if operator == "?":
+    joiner = "&"
+    varprefix = varname + "=" 
+  if type(value) == type([]):
+    if 0 == len(value):
+      return ""
+    if explode == "+":
+      return joiner.join([varname + "=" + urllib.quote(x, safe) for x in value])
+    elif explode == "*":
+      return joiner.join([urllib.quote(x, safe) for x in value])
+    else:
+      return varprefix + ",".join([urllib.quote(x, safe) for x in value])
+  elif type(value) == type({}):
+    if 0 == len(value):
+      return ""
+    keys = value.keys()
+    keys.sort()
+    if explode == "+":
+      return joiner.join([varname + "." + urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys])
+    elif explode == "*":
+      return joiner.join([urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys])
+    else:
+      return varprefix + ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
+  else:
+    if value:
+      return varname + "=" + urllib.quote(value, safe)
+    else:
+      return varname 
+
+TOSTRING = {
+    "" : _tostring,
+    "+": _tostring,
+    ";": _tostring_query,
+    "?": _tostring_query,
+    "/": _tostring_path,
+    ".": _tostring_path,
+    }
+
+
+def expand(template, vars):
+  def _sub(match):
+    groupdict = match.groupdict()
+    operator = groupdict.get('operator')
+    if operator is None:
+      operator = ''
+    varlist = groupdict.get('varlist')
+
+    safe = ""
+    if operator == '+':
+      safe = RESERVED
+    varspecs = varlist.split(",")
+    varnames = []
+    defaults = {}
+    for varspec in varspecs:
+      m = VAR.search(varspec)
+      groupdict = m.groupdict()
+      varname = groupdict.get('varname')
+      explode = groupdict.get('explode')
+      partial = groupdict.get('partial')
+      default = groupdict.get('default')
+      if default:
+        defaults[varname] = default
+      varnames.append((varname, explode, partial))
+
+    retval = []
+    joiner = operator
+    prefix = operator
+    if operator == "+":
+      prefix = ""
+      joiner = ","
+    if operator == "?":
+      joiner = "&"
+    if operator == "":
+      joiner = ","
+    for varname, explode, partial in varnames:
+      if varname in vars:
+        value = vars[varname]
+        #if not value and (type(value) == type({}) or type(value) == type([])) and varname in defaults:
+        if not value and value != "" and varname in defaults:
+          value = defaults[varname]
+      elif varname in defaults:
+        value = defaults[varname]
+      else:
+        continue
+      retval.append(TOSTRING[operator](varname, value, explode, operator, safe=safe))
+    if "".join(retval):
+      return prefix + joiner.join(retval)
+    else:
+      return ""
+
+  return TEMPLATE.sub(_sub, template)