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)