Move to using setuptools exclusively.

Take the opportunity to clean up contrib/ as all that functionality
has been subsumed in the oauth2decorator.

Reviwed in http://codereview.appspot.com/5271053/
This commit is contained in:
Joe Gregorio
2011-10-17 13:25:24 -04:00
parent f2f8a5a4ac
commit d667c9d105
9 changed files with 51 additions and 473 deletions

View File

@@ -30,6 +30,7 @@ import gflags
import logging
import sys
import os
import pkg_resources
from distutils.dir_util import copy_tree
from distutils.file_util import copy_file
@@ -72,13 +73,13 @@ def find_source(module):
basename = os.path.basename(m.__file__)
if basename.startswith('__init__.'):
isdir = True
location = os.path.dirname(m.__file__)
location = os.path.dirname(
pkg_resources.resource_filename(module, '__init__.py'))
else:
if os.path.isfile(m.__file__):
location = m.__file__.rsplit('.', 1)[0] + '.py'
else:
# The file is an egg, extract to a temporary location
import pkg_resources
location = pkg_resources.resource_filename(module, module + '.py')
return (isdir, location)
@@ -103,8 +104,8 @@ def main(argv):
# Check if the supplied directory is an App Engine project by looking
# for an app.yaml
if not os.path.isfile(os.path.join(dir, 'app.yaml')):
sys.exit('The given directory is not a Google App Engine project: %s' % dir)
sys.exit('The given directory is not a Google App Engine project: %s' %
dir)
# Build up the set of file or directory copying actions we need to do
action = [] # (src, dst, isdir)

View File

@@ -1,173 +0,0 @@
# 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.
from google.appengine.api import users
from google.appengine.ext import db
import apiclient.ext.appengine
import logging
import simple_wrapper
class Flow(db.Model):
flow = apiclient.ext.appengine.FlowThreeLeggedProperty()
class Credentials(db.Model):
credentials = apiclient.ext.appengine.OAuthCredentialsProperty()
class oauth_required(object):
def __init__(self, *decorator_args, **decorator_kwargs):
"""A decorator to require that a user has gone through the OAuth dance before accessing a handler.
To use it, decorate your get() method like this:
@oauth_required
def get(self):
buzz_wrapper = oauth_handlers.build_buzz_wrapper_for_current_user()
user_profile_data = buzz_wrapper.get_profile()
self.response.out.write('Hello, ' + user_profile_data.displayName)
We will redirect the user to the OAuth endpoint and afterwards the OAuth
will send the user back to the DanceFinishingHandler that you have configured.
This should only used for GET requests since any payload in a POST request
will be lost. Any parameters in the original URL will be preserved.
"""
self.decorator_args = decorator_args
self.decorator_kwargs = decorator_kwargs
def __load_settings_from_file__(self):
# Load settings from settings.py module if it's available
# Only return the keys that the user has explicitly set
try:
import settings
# This uses getattr so that the user can set just the parameters they care about
flow_settings = {
'consumer_key' : getattr(settings, 'CONSUMER_KEY', None),
'consumer_secret' : getattr(settings, 'CONSUMER_SECRET', None),
'user_agent' : getattr(settings, 'USER_AGENT', None),
'domain' : getattr(settings, 'DOMAIN', None),
'scope' : getattr(settings, 'SCOPE', None),
'xoauth_display_name' : getattr(settings, 'XOAUTH_DISPLAY_NAME', None)
}
# Strip out all the keys that weren't specified in the settings.py
# This is needed to ensure that those keys don't override what's
# specified in the decorator invocation
cleaned_flow_settings = {}
for key,value in flow_settings.items():
if value is not None:
cleaned_flow_settings[key] = value
return cleaned_flow_settings
except ImportError:
return {}
def __load_settings__(self):
# Set up the default arguments and override them with whatever values have been given to the decorator
flow_settings = {
'consumer_key' : 'anonymous',
'consumer_secret' : 'anonymous',
'user_agent' : 'google-api-client-python-buzz-webapp/1.0',
'domain' : 'anonymous',
'scope' : 'https://www.googleapis.com/auth/buzz',
'xoauth_display_name' : 'Default Display Name For OAuth Application'
}
logging.info('OAuth settings: %s ' % flow_settings)
# Override the defaults with whatever the user may have put into settings.py
settings_kwargs = self.__load_settings_from_file__()
flow_settings.update(settings_kwargs)
logging.info('OAuth settings: %s ' % flow_settings)
# Override the defaults with whatever the user have specified in the decorator's invocation
flow_settings.update(self.decorator_kwargs)
logging.info('OAuth settings: %s ' % flow_settings)
return flow_settings
def __call__(self, handler_method):
def check_oauth_credentials_wrapper(*args, **kwargs):
handler_instance = args[0]
# TODO(ade) Add support for POST requests
if handler_instance.request.method != 'GET':
raise webapp.Error('The check_oauth decorator can only be used for GET '
'requests')
# Is this a request from the OAuth system after finishing the OAuth dance?
if handler_instance.request.get('oauth_verifier'):
user = users.get_current_user()
logging.debug('Finished OAuth dance for: %s' % user.email())
f = Flow.get_by_key_name(user.user_id())
if f:
credentials = f.flow.step2_exchange(handler_instance.request.params)
c = Credentials(key_name=user.user_id(), credentials=credentials)
c.put()
# We delete the flow so that a malicious actor can't pretend to be the OAuth service
# and replace a valid token with an invalid token
f.delete()
handler_method(*args)
return
# Find out who the user is. If we don't know who you are then we can't
# look up your OAuth credentials thus we must ensure the user is logged in.
user = users.get_current_user()
if not user:
handler_instance.redirect(users.create_login_url(handler_instance.request.uri))
return
# Now that we know who the user is look up their OAuth credentials
# if we don't find the credentials then send them through the OAuth dance
if not Credentials.get_by_key_name(user.user_id()):
flow_settings = self.__load_settings__()
p = apiclient.discovery.build("buzz", "v1")
flow = apiclient.oauth.FlowThreeLegged(p.auth_discovery(),
consumer_key=flow_settings['consumer_key'],
consumer_secret=flow_settings['consumer_secret'],
user_agent=flow_settings['user_agent'],
domain=flow_settings['domain'],
scope=flow_settings['scope'],
xoauth_displayname=flow_settings['xoauth_display_name'])
# The OAuth system needs to send the user right back here so that they
# get to the page they originally intended to visit.
oauth_return_url = handler_instance.request.uri
authorize_url = flow.step1_get_authorize_url(oauth_return_url)
f = Flow(key_name=user.user_id(), flow=flow)
f.put()
handler_instance.redirect(authorize_url)
return
# If the user already has a token then call the wrapped handler
handler_method(*args)
return check_oauth_credentials_wrapper
def build_buzz_wrapper_for_current_user(api_key=None):
user = users.get_current_user()
credentials = Credentials.get_by_key_name(user.user_id()).credentials
if not api_key:
try:
import settings
api_key = getattr(settings, 'API_KEY', None)
except ImportError:
return {}
return simple_wrapper.SimpleWrapper(api_key=api_key,
credentials=credentials)

View File

@@ -1,82 +0,0 @@
#!/usr/bin/python2.4
#
# 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.
__author__ = 'ade@google.com (Ade Oshineye)'
import apiclient.discovery
import httplib2
import logging
class SimpleWrapper(object):
"Simple client that exposes the bare minimum set of common Buzz operations"
def __init__(self, api_key=None, credentials=None):
self.http = httplib2.Http()
if credentials:
logging.debug('Using api_client with credentials')
self.http = credentials.authorize(self.http)
self.api_client = apiclient.discovery.build('buzz', 'v1', http=self.http, developerKey=api_key)
else:
logging.debug('Using api_client that doesn\'t have credentials')
self.api_client = apiclient.discovery.build('buzz', 'v1', http=self.http, developerKey=api_key)
def search(self, query, user_token=None, max_results=10):
if query is None or query.strip() is '':
return None
json = self.api_client.activities().search(q=query, max_results=max_results).execute()
if json.has_key('items'):
return json['items']
return []
def post(self, message_body, user_id='@me'):
if message_body is None or message_body.strip() is '':
return None
activities = self.api_client.activities()
logging.info('Retrieved activities for: %s' % user_id)
activity = activities.insert(userId=user_id, body={
'data' : {
'title': message_body,
'object': {
'content': message_body,
'type': 'note'}
}
}
).execute()
url = activity['links']['alternate'][0]['href']
logging.info('Just created: %s' % url)
return url
def get_profile(self, user_id='@me'):
user_profile_data = self.api_client.people().get(userId=user_id).execute()
return user_profile_data
def get_follower_count(self, user_id='@me'):
return self.__get_group_count(user_id, '@followers')
def get_following_count(self, user_id='@me'):
return self.__get_group_count(user_id, '@following')
def __get_group_count(self, user_id, group_id):
# Fetching 0 results is a performance optimisation that minimises the
# amount of data that's getting retrieved from the server
cmd = self.api_client.people().list(userId=user_id, groupId=group_id,
max_results=0)
members = cmd.execute()
if 'totalResults' not in members.keys():
return -1
return members['totalResults']

View File

@@ -1,118 +0,0 @@
#!/usr/bin/python2.4
#
# Copyright 2010 Google Inc. All Rights Reserved.
__author__ = 'ade@google.com (Ade Oshineye)'
from contrib.buzz.simple_wrapper import SimpleWrapper
import apiclient.oauth
import httplib2
import logging
import oauth2 as oauth
import os
import pickle
import unittest
class SimpleWrapperTest(unittest.TestCase):
# None of these tests make a remote call. We assume the underlying libraries
# and servers are working.
def test_wrapper_rejects_empty_post(self):
wrapper = SimpleWrapper()
self.assertEquals(None, wrapper.post('', '108242092577082601423'))
def test_wrapper_rejects_post_containing_only_whitespace(self):
wrapper = SimpleWrapper()
self.assertEquals(None, wrapper.post(' ', '108242092577082601423'))
def test_wrapper_rejects_none_post(self):
wrapper = SimpleWrapper()
self.assertEquals(None, wrapper.post(None, '108242092577082601423'))
def test_wrapper_rejects_empty_search(self):
wrapper = SimpleWrapper()
self.assertEquals(None, wrapper.search(''))
def test_wrapper_rejects_search_containing_only_whitespace(self):
wrapper = SimpleWrapper()
self.assertEquals(None, wrapper.search(' '))
def test_wrapper_rejects_search_with_none(self):
wrapper = SimpleWrapper()
self.assertEquals(None, wrapper.search(None))
def test_wrapper_returns_minus_one_for_hidden_follower_count(self):
wrapper = SimpleWrapper()
self.assertEquals(-1, wrapper.get_follower_count(user_id='108242092577082601423'))
def test_wrapper_returns_positive_value_for_visible_follower_count(self):
wrapper = SimpleWrapper()
count = wrapper.get_follower_count(user_id='googlebuzz')
self.assertTrue(count > 0, "Got %s instead" % count)
def test_wrapper_returns_minus_one_for_hidden_following_count(self):
wrapper = SimpleWrapper()
self.assertEquals(-1, wrapper.get_following_count(user_id='108242092577082601423'))
def test_wrapper_returns_positive_value_for_visible_following_count(self):
wrapper = SimpleWrapper()
count = wrapper.get_following_count(user_id='googlebuzz')
self.assertTrue(count > 0, "Got %s instead" % count)
class SimpleWrapperRemoteTest(unittest.TestCase):
# These tests make remote calls
def __init__(self, method_name):
unittest.TestCase.__init__(self, method_name)
oauth_params_dict = {}
for line in open('./contrib_tests/test_account.oacurl.properties'):
line = line.strip()
if line.startswith('#'):
continue
key,value = line.split('=')
oauth_params_dict[key.strip()] = value.strip()
consumer = oauth.Consumer(oauth_params_dict['consumerKey'],
oauth_params_dict['consumerSecret'])
token = oauth.Token(oauth_params_dict['accessToken'],
oauth_params_dict['accessTokenSecret'])
user_agent = 'google-api-client-python-buzz-webapp/1.0'
credentials = apiclient.oauth.OAuthCredentials(consumer, token, user_agent)
self.wrapper = SimpleWrapper(credentials=credentials)
def test_searching_returns_results(self):
results = self.wrapper.search('oshineye')
self.assertTrue(results is not None)
def test_searching_honours_max_results(self):
max = 5
results = self.wrapper.search('oshineye', max_results=max)
self.assertEquals(max, len(results))
def test_can_fetch_profile(self):
profile = self.wrapper.get_profile('googlebuzz')
self.assertTrue(profile is not None)
profile = self.wrapper.get_profile(user_id='adewale')
self.assertTrue(profile is not None)
def test_can_post_without_user_id(self):
url = self.wrapper.post('test message')
self.assertTrue(url is not None)
self.assertTrue(url.startswith('https://profiles.google.com/'), url)
def test_can_post_with_user_id(self):
url = self.wrapper.post('test message', '108242092577082601423')
self.assertTrue(url is not None)
self.assertTrue(url.startswith('https://profiles.google.com/'), url)
def test_wrapper_returns_positive_value_for_hidden_follower_count_when_authorised(self):
count = self.wrapper.get_follower_count(user_id='108242092577082601423')
self.assertTrue(count > 0, "Got %s instead" % count)
def test_wrapper_returns_positive_value_for_hidden_following_count_when_authorised(self):
count = self.wrapper.get_following_count(user_id='108242092577082601423')
self.assertTrue(count > 0, "Got %s instead" % count)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,6 +0,0 @@
#Mon Oct 04 23:45:49 PDT 2010
#A set of credentials for posting as http://www.google.com/profiles/108242092577082601423
consumerSecret=anonymous
accessToken=1/80QKKG4CbMwOZjmW1udam-fVaiUOY1zO-8u3dhiLK6g
consumerKey=anonymous
accessTokenSecret=R6CnehJTZf9aKuSMtgkmX7KZ

View File

@@ -18,7 +18,7 @@
"""Copy files from source to dest expanding symlinks along the way.
"""
from distutils.dir_util import copy_tree
from shutil import copytree
import gflags
import sys
@@ -26,10 +26,27 @@ import sys
FLAGS = gflags.FLAGS
# Ignore these files and directories when copying over files into the snapshot.
IGNORE = set(['.hg', 'httplib2', 'oauth2', 'simplejson', 'static', 'gflags.py',
'gflags_validators.py'])
# In addition to the above files also ignore these files and directories when
# copying over samples into the snapshot.
IGNORE_IN_SAMPLES = set(['apiclient', 'oauth2client', 'uritemplate'])
gflags.DEFINE_string('source', '.', 'Directory name to copy from.')
gflags.DEFINE_string('dest', 'snapshot', 'Directory name to copy to.')
def _ignore(path, names):
retval = set()
if path != '.':
retval = retval.union(IGNORE_IN_SAMPLES.intersection(names))
retval = retval.union(IGNORE.intersection(names))
return retval
def main(argv):
# Let the gflags module process the command-line arguments
try:
@@ -38,7 +55,8 @@ def main(argv):
print '%s\\nUsage: %s ARGS\\n%s' % (e, argv[0], FLAGS)
sys.exit(1)
copy_tree(FLAGS.source, FLAGS.dest, verbose=True)
copytree(FLAGS.source, FLAGS.dest, symlinks=True,
ignore=_ignore)
if __name__ == '__main__':

View File

@@ -17,8 +17,6 @@
Also installs included versions of third party libraries, if those libraries
are not already installed.
"""
import setup_utils
from setuptools import setup
packages = [
@@ -30,30 +28,28 @@ packages = [
'apiclient.contrib.latitude',
'apiclient.contrib.moderator',
'uritemplate',
]
]
install_requires = []
py_modules = []
install_requires = [
'httplib2',
'oauth2',
'python-gflags',
]
try:
import json
needs_json = False
except ImportError:
needs_json = True
# (module to test for, install_requires to add if missing, packages to add if missing, py_modules to add if missing)
REQUIREMENTS = [
('httplib2', 'httplib2', 'httplib2', None),
('oauth2', 'oauth2', 'oauth2', None),
('gflags', 'python-gflags', None, ['gflags', 'gflags_validators']),
(['json', 'simplejson', 'django.utils'], 'simplejson', 'simplejson', None)
]
for import_name, requires, package, modules in REQUIREMENTS:
if setup_utils.is_missing(import_name):
install_requires.append(requires)
if needs_json:
install_requires.append('simplejson')
long_desc = """The Google API Client for Python is a client library for
accessing the Buzz, Moderator, and Latitude APIs."""
setup(name="google-api-python-client",
version="1.0beta4",
version="1.0beta5prerelease",
description="Google API Client Library for Python",
long_description=long_desc,
author="Joe Gregorio",
@@ -61,11 +57,9 @@ setup(name="google-api-python-client",
url="http://code.google.com/p/google-api-python-client/",
install_requires=install_requires,
packages=packages,
py_modules=py_modules,
package_data={
'apiclient': ['contrib/*/*.json']
},
scripts=['bin/enable-app-engine-project'],
license="Apache 2.0",
keywords="google api client",
classifiers=['Development Status :: 4 - Beta',

View File

@@ -17,34 +17,30 @@
Also installs included versions of third party libraries, if those libraries
are not already installed.
"""
import setup_utils
from setuptools import setup
packages = [
'oauth2client',
]
install_requires = []
py_modules = []
install_requires = [
'httplib2',
'python-gflags',
]
try:
import json
needs_json = False
except ImportError
needs_json = True
# (module to test for, install_requires to add if missing, packages to add if missing, py_modules to add if missing)
REQUIREMENTS = [
('httplib2', 'httplib2', 'httplib2', None),
('gflags', 'python-gflags', None, ['gflags', 'gflags_validators']),
(['json', 'simplejson', 'django.utils'], 'simplejson', 'simplejson', None)
]
for import_name, requires, package, modules in REQUIREMENTS:
if setup_utils.is_missing(import_name):
install_requires.append(requires)
if needs_json:
install_requires.append('simplejson')
long_desc = """The oauth2client is a client library for OAuth 2.0."""
setup(name="oauth2client",
version="1.0beta4",
version="1.0beta5prerelease",
description="OAuth 2.0 client library",
long_description=long_desc,
author="Joe Gregorio",

View File

@@ -1,52 +0,0 @@
# 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.
"""Utility functions for setup.py file(s)."""
__author__ = 'tom.h.miller@gmail.com (Tom Miller)'
import sys
def is_missing(packages):
"""Return True if a package can't be imported."""
retval = True
sys_path_original = sys.path[:]
# Remove the current directory from the list of paths to check when
# importing modules.
try:
# Sometimes it's represented by an empty string?
sys.path.remove('')
except ValueError:
import os.path
try:
sys.path.remove(os.path.abspath(os.path.curdir))
except ValueError:
pass
if not isinstance(packages, type([])):
packages = [packages]
for name in packages:
try:
__import__(name)
retval = False
except ImportError:
retval = True
if retval == False:
break
sys.path = sys_path_original
return retval