diff --git a/.hgignore b/.hgignore index 274be82..3a3f338 100644 --- a/.hgignore +++ b/.hgignore @@ -7,3 +7,4 @@ syntax: glob samples/cmdline/*.dat htmlcov/* .coverage +database.sqlite3 diff --git a/apiclient/contrib/moderator/future.json b/apiclient/contrib/moderator/future.json index 7e3d978..87d525b 100644 --- a/apiclient/contrib/moderator/future.json +++ b/apiclient/contrib/moderator/future.json @@ -2,7 +2,7 @@ "data": { "moderator": { "v1": { - "baseUrl": "https://www.googleapis.com/", + "baseUrl": "https://www.googleapis.com/", "auth": { "request": { "url": "https://www.google.com/accounts/OAuthGetRequestToken", diff --git a/apiclient/discovery.py b/apiclient/discovery.py index 328d970..945db74 100644 --- a/apiclient/discovery.py +++ b/apiclient/discovery.py @@ -45,13 +45,21 @@ except ImportError: # pragma: no cover import json as simplejson -class HttpError(Exception): +class Error(Exception): + """Base error for this module.""" pass -class UnknownLinkType(Exception): +class HttpError(Error): + """HTTP data was invalid or unexpected.""" pass + +class UnknownLinkType(Error): + """Link type unknown or unexpected.""" + pass + + DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe' '{?api,apiVersion}') @@ -86,12 +94,15 @@ class JsonModel(object): if body_value is None: return (headers, path_params, query, None) else: - model = {'data': body_value} + if len(body_value) == 1 and 'data' in body_value: + model = body_value + else: + model = {'data': body_value} headers['content-type'] = 'application/json' return (headers, path_params, query, simplejson.dumps(model)) def build_query(self, params): - params.update({'alt': 'json', 'prettyprint': 'true'}) + params.update({'alt': 'json'}) astuples = [] for key, value in params.iteritems(): if getattr(value, 'encode', False) and callable(value.encode): @@ -103,6 +114,9 @@ class JsonModel(object): # Error handling is TBD, for example, do we retry # for some operation/error combinations? if resp.status < 300: + if resp.status == 204: + # A 204: No Content response should be treated differently to all the other success states + return simplejson.loads('{}') return simplejson.loads(content)['data'] else: logging.debug('Content from bad request was: %s' % content) diff --git a/apiclient/oauth.py b/apiclient/oauth.py index 8b827c6..9907c46 100644 --- a/apiclient/oauth.py +++ b/apiclient/oauth.py @@ -21,7 +21,17 @@ except ImportError: from cgi import parse_qs, parse_qsl -class MissingParameter(Exception): +class Error(Exception): + """Base error for this module.""" + pass + + +class RequestError(Error): + """Error occurred during request.""" + pass + + +class MissingParameter(Error): pass @@ -120,8 +130,10 @@ class OAuthCredentials(Credentials): if headers == None: headers = {} headers.update(req.to_header()) - if 'user-agent' not in headers: - headers['user-agent'] = self.user_agent + if 'user-agent' in headers: + headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] + else: + headers['user-agent'] = self.user_agent return request_orig(uri, method, body, headers, redirections, connection_type) @@ -185,7 +197,7 @@ class FlowThreeLegged(object): body=body) if resp['status'] != '200': logging.error('Failed to retrieve temporary authorization: %s' % content) - raise Exception('Invalid response %s.' % resp['status']) + raise RequestError('Invalid response %s.' % resp['status']) self.request_token = dict(parse_qsl(content)) @@ -222,7 +234,7 @@ class FlowThreeLegged(object): resp, content = client.request(uri, 'POST', headers=headers) if resp['status'] != '200': logging.error('Failed to retrieve access token: %s' % content) - raise Exception('Invalid response %s.' % resp['status']) + raise RequestError('Invalid response %s.' % resp['status']) oauth_params = dict(parse_qsl(content)) token = oauth.Token( diff --git a/buzz_gae_client.py b/buzz_gae_client.py index ffc74c3..f790e02 100644 --- a/buzz_gae_client.py +++ b/buzz_gae_client.py @@ -36,6 +36,17 @@ REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken?domain AUTHORIZE_URL = 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?domain=anonymous&scope=https://www.googleapis.com/auth/buzz' ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken' + +class Error(Exception): + """Base error for this module.""" + pass + + +class RequestError(Error): + """Request returned failure or unexpected data.""" + pass + + # TODO(ade) This class is really a BuzzGaeBuilder. Rename it. class BuzzGaeClient(object): def __init__(self, consumer_key='anonymous', consumer_secret='anonymous'): @@ -49,7 +60,7 @@ class BuzzGaeClient(object): if resp['status'] != '200': logging.warn('Request: %s failed with status: %s. Content was: %s' % (url, resp['status'], content)) - raise Exception('Invalid response %s.' % resp['status']) + raise RequestError('Invalid response %s.' % resp['status']) return resp, content def get_request_token(self, callback_url, display_name = None): diff --git a/functional_tests/test_services.py b/functional_tests/test_services.py index 6403946..a849577 100644 --- a/functional_tests/test_services.py +++ b/functional_tests/test_services.py @@ -10,6 +10,7 @@ These tests are read-only in order to ensure they're repeatable. They also only work with publicly visible data in order to avoid dealing with OAuth. """ import httplib2 +import pprint __author__ = 'ade@google.com (Ade Oshineye)' @@ -18,6 +19,7 @@ import httplib2 import logging import pickle import os +import time import unittest # TODO(ade) Remove this mock once the bug in the discovery document is fixed @@ -34,29 +36,43 @@ class HttpMock(object): return httplib2.Response(self.headers), self.data class BuzzFunctionalTest(unittest.TestCase): + def test_can_get_specific_activity(self): + buzz = build('buzz', 'v1') + activity = buzz.activities().get(userId='105037104815911535953', + postId='B:z12sspviqyakfvye123wehng0muwz5jzq04').execute() + + self.assertTrue(activity is not None) + + def test_can_get_specific_activity_with_tag_id(self): + buzz = build('buzz', 'v1') + activity = buzz.activities().get(userId='105037104815911535953', + postId='tag:google.com,2010:buzz:z13ptnw5usmnv15ey22fzlswnuqoebasu').execute() + + self.assertTrue(activity is not None) + def test_can_get_buzz_activities_with_many_params(self): buzz = build('buzz', 'v1') max_results = 2 - actcol = buzz.activities() - activities = actcol.list(userId='googlebuzz', scope='@self', + activities_command = buzz.activities() + activities = activities_command.list(userId='googlebuzz', scope='@self', max_comments=max_results*2 ,max_liked=max_results*3, max_results=max_results).execute() activity_count = len(activities['items']) self.assertEquals(max_results, activity_count) - activities = actcol.list_next(activities).execute() + activities = activities_command.list_next(activities).execute() activity_count = len(activities['items']) self.assertEquals(max_results, activity_count) def test_can_get_multiple_pages_of_buzz_activities(self): buzz = build('buzz', 'v1') max_results = 2 - actcol = buzz.activities() + activities_command = buzz.activities() - activities = actcol.list(userId='adewale', scope='@self', + activities = activities_command.list(userId='adewale', scope='@self', max_results=max_results).execute() for count in range(10): - activities = actcol.list_next(activities).execute() + activities = activities_command.list_next(activities).execute() activity_count = len(activities['items']) self.assertEquals(max_results, activity_count, 'Failed after %s pages' % str(count)) @@ -86,6 +102,31 @@ class BuzzFunctionalTest(unittest.TestCase): self.assertEquals('111062888259659218284', person['id']) self.assertEquals('http://www.google.com/profiles/googlebuzz', person['profileUrl']) + def test_can_get_followees_of_user(self): + buzz = build('buzz', 'v1') + expected_followees = 30 + following = buzz.people().list(userId='googlebuzz', groupId='@following', max_results=expected_followees).execute() + + self.assertEquals(expected_followees, following['totalResults']) + self.assertEquals(expected_followees, len(following['entry'])) + + def test_can_efficiently_get_follower_count_of_user(self): + buzz = build('buzz', 'v1') + + # Restricting max_results to 1 means only a tiny amount of data comes back but the totalResults still has the total. + following = buzz.people().list(userId='googlebuzz', groupId='@followers', max_results=1).execute() + + # @googlebuzz has a large but fluctuating number of followers + # It is sufficient if the result is bigger than 10, 000 + follower_count = following['totalResults'] + self.assertTrue(follower_count > 10000, follower_count) + + def test_follower_count_is_zero_for_user_with_hidden_follower_count(self): + buzz = build('buzz', 'v1') + following = buzz.people().list(userId='adewale', groupId='@followers').execute() + + self.assertEquals(0, following['totalResults']) + class BuzzAuthenticatedFunctionalTest(unittest.TestCase): def __init__(self, method_name): @@ -97,8 +138,36 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): self.http = credentials.authorize(httplib2.Http()) - def test_can_list_groups_belonging_to_user(self): - # TODO(ade) This should not require authentication. It does because we're adding a spurious @self to the URL + def test_can_create_activity(self): + buzz = build('buzz', 'v1', http=self.http) + + activity = buzz.activities().insert(userId='@me', body={ + 'title': 'Testing insert', + 'object': { + 'content': u'Just a short note to show that insert is working. ?', + 'type': 'note'} + } + ).execute() + self.assertTrue(activity is not None) + + def test_can_create_private_activity(self): + buzz = build('buzz', 'v1', http=self.http) + + activity = buzz.activities().insert(userId='@me', body={ + 'title': 'Testing insert', + 'object': { + 'content': 'This is a private post.' + }, + 'visibility': { + 'entries': [ + { 'id': 'tag:google.com,2010:buzz-group:108242092577082601423:13' } + ] + } + } + ).execute() + self.assertTrue(activity is not None) + + def test_can_identify_number_of_groups_belonging_to_user(self): buzz = build('buzz', 'v1', http=self.http) groups = buzz.groups().list(userId='108242092577082601423').execute() @@ -106,14 +175,75 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): expected_default_number_of_groups = 4 self.assertEquals(expected_default_number_of_groups, len(groups['items'])) - def IGNORE__test_can_get_followees_of_user(self): - # This currently fails with: - # Attempting to access self view of a different user. - # and URL: + def IGNORE__test_can_like_activity(self): buzz = build('buzz', 'v1', http=self.http) - following = buzz.groups().get(userId='googlebuzz', groupId='@following').execute() + activity = buzz.activities().insert(userId='@me', body={ + 'title': 'Testing insert', + 'object': { + 'content': u'Just a short note to show that insert is working. ?', + 'type': 'note'} + } + ).execute() + pprint.pprint(activity) + id = activity['id'] + likers = buzz.people().liked(userId='105037104815911535953', postId=id, groupId='@liked', scope='@self').execute() + # Todo(ade) Insert the new liker once the Buzz back-end bug is fixed - self.assertEquals(17, len(following)) + def test_can_comment_on_activity(self): + buzz = build('buzz', 'v1', http=self.http) + + activity = buzz.activities().insert(userId='@me', body={ + 'title': 'A new activity', + 'object': { + 'content': u'The body of the new activity', + 'type': 'note'} + } + ).execute() + + id = activity['id'] + comment = buzz.comments().insert(userId='@me', postId=id, body={ + "content": "A comment on the new activity" + }).execute() + + def IGNORE__test_can_list_groups_belonging_to_user(self): + # TODO(ade) Uncomment this test once the related Buzz back-end bug is fixed + buzz = build('buzz', 'v1', http=self.http) + groups = buzz.groups().list(userId='108242092577082601423').execute() + pprint.pprint(groups) + + group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:15').execute() + self.assertEquals('G:108242092577082601423:15', group['id'], group) + + group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:14').execute() + self.assertEquals('G:108242092577082601423:14', group['id'], group) + + group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:13').execute() + self.assertEquals('G:108242092577082601423:13', group['id'], group) + + group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:6').execute() + self.assertEquals('G:108242092577082601423:6', group['id'], group) + + group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:9999999').execute() + self.assertEquals(None, group, group) + + def test_can_delete_activity(self): + buzz = build('buzz', 'v1', http=self.http) + + activity = buzz.activities().insert(userId='@me', body={ + 'title': 'Activity to be deleted', + 'object': { + 'content': u'Created this activity so that it can be deleted.', + 'type': 'note'} + } + ).execute() + id = activity['id'] + + buzz.activities().delete(scope='@self', userId='@me', postId=id).execute() + time.sleep(2) + + activity_url = activity['links']['self'][0]['href'] + resp, content = self.http.request(activity_url, 'GET') + self.assertEquals(404, resp.status) if __name__ == '__main__': unittest.main() diff --git a/httplib2/__init__.py b/httplib2/__init__.py index 61e9caa..567e24e 100644 --- a/httplib2/__init__.py +++ b/httplib2/__init__.py @@ -55,9 +55,9 @@ from gettext import gettext as _ import socket try: - from httplib2 import socks + from httplib2 import socks except ImportError: - socks = None + socks = None # Build the appropriate socket wrapper for ssl try: @@ -83,7 +83,7 @@ def has_timeout(timeout): # python 2.6 __all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', - 'debuglevel'] + 'debuglevel', 'ProxiesUnavailableError'] # The httplib debug level, set to a non-zero value to get debug output @@ -125,6 +125,7 @@ class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass class RelativeURIError(HttpLib2Error): pass class ServerNotFoundError(HttpLib2Error): pass +class ProxiesUnavailableError(HttpLib2Error): pass # Open Items: # ----------- @@ -721,6 +722,9 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): def connect(self): """Connect to the host and port specified in __init__.""" # Mostly verbatim from httplib.py. + if self.proxy_info and socks is None: + raise ProxiesUnavailableError( + 'Proxy support missing but proxy use was requested!') msg = "getaddrinfo returns an empty list" for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): diff --git a/httplib2/socks.py b/httplib2/socks.py index 6f4f020..b65fb38 100644 --- a/httplib2/socks.py +++ b/httplib2/socks.py @@ -41,12 +41,13 @@ mainly to merge bug fixes found in Sourceforge """ import socket + +if getattr(socket, 'socket', None) is None: + raise ImportError('socket.socket missing, proxy support unusable') + import struct import sys -if not hasattr(socket, 'socket'): - raise ImportError("Running on App Engine?") - PROXY_TYPE_SOCKS4 = 1 PROXY_TYPE_SOCKS5 = 2 PROXY_TYPE_HTTP = 3 diff --git a/samples/api-python-client-doc/README b/samples/api-python-client-doc/README new file mode 100644 index 0000000..835a60c --- /dev/null +++ b/samples/api-python-client-doc/README @@ -0,0 +1,6 @@ +This sample is the code that drives + + http://api-python-client-doc.appspot.com/ + +It is an application that serves up the Python help documentation +for each API. diff --git a/samples/helpdoc/apiclient b/samples/api-python-client-doc/apiclient similarity index 100% rename from samples/helpdoc/apiclient rename to samples/api-python-client-doc/apiclient diff --git a/samples/helpdoc/app.yaml b/samples/api-python-client-doc/app.yaml similarity index 100% rename from samples/helpdoc/app.yaml rename to samples/api-python-client-doc/app.yaml diff --git a/samples/helpdoc/httplib2 b/samples/api-python-client-doc/httplib2 similarity index 100% rename from samples/helpdoc/httplib2 rename to samples/api-python-client-doc/httplib2 diff --git a/samples/helpdoc/index.yaml b/samples/api-python-client-doc/index.yaml similarity index 100% rename from samples/helpdoc/index.yaml rename to samples/api-python-client-doc/index.yaml diff --git a/samples/helpdoc/main.py b/samples/api-python-client-doc/main.py similarity index 100% rename from samples/helpdoc/main.py rename to samples/api-python-client-doc/main.py diff --git a/samples/helpdoc/oauth2 b/samples/api-python-client-doc/oauth2 similarity index 100% rename from samples/helpdoc/oauth2 rename to samples/api-python-client-doc/oauth2 diff --git a/samples/helpdoc/simplejson b/samples/api-python-client-doc/simplejson similarity index 100% rename from samples/helpdoc/simplejson rename to samples/api-python-client-doc/simplejson diff --git a/samples/helpdoc/uritemplate b/samples/api-python-client-doc/uritemplate similarity index 100% rename from samples/helpdoc/uritemplate rename to samples/api-python-client-doc/uritemplate diff --git a/samples/buzz/buzz.py b/samples/buzz/buzz.py new file mode 100644 index 0000000..14b5ea1 --- /dev/null +++ b/samples/buzz/buzz.py @@ -0,0 +1,66 @@ +#!/usr/bin/python2.4 +# -*- coding: utf-8 -*- +# +# Copyright 2010 Google Inc. All Rights Reserved. + +"""Simple command-line example for Buzz. + +Command-line application that retrieves the users +latest content and then adds a new entry. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +from apiclient.discovery import build + +import httplib2 +import pickle +import pprint + +# Uncomment the next line to get very detailed logging +# httplib2.debuglevel = 4 + +def main(): + f = open("buzz.dat", "r") + credentials = pickle.loads(f.read()) + f.close() + + http = httplib2.Http() + http = credentials.authorize(http) + + p = build("buzz", "v1", http=http) + activities = p.activities() + + # Retrieve the first two activities + activitylist = activities.list(max_results='2', scope='@self', userId='@me').execute() + print "Retrieved the first two activities" + + # Retrieve the next two activities + activitylist = activities.list_next(activitylist).execute() + print "Retrieved the next two activities" + + # Add a new activity + new_activity_body = { + 'title': 'Testing insert', + 'object': { + 'content': u'Just a short note to show that insert is working. ☄', + 'type': 'note'} + } + activity = activities.insert(userId='@me', body=new_activity_body).execute() + print "Added a new activity" + + activitylist = activities.list(max_results='2', scope='@self', userId='@me').execute() + + # Add a comment to that activity + comment_body = { + "content": "This is a comment" + } + item = activitylist['items'][0] + comment = p.comments().insert( + userId=item['actor']['id'], postId=item['id'], body=comment_body + ).execute() + print 'Added a comment to the new activity' + pprint.pprint(comment) + +if __name__ == '__main__': + main() diff --git a/samples/cmdline/three_legged_dance.py b/samples/buzz/three_legged_dance.py similarity index 94% rename from samples/cmdline/three_legged_dance.py rename to samples/buzz/three_legged_dance.py index ff1d657..9972455 100644 --- a/samples/cmdline/three_legged_dance.py +++ b/samples/buzz/three_legged_dance.py @@ -22,11 +22,6 @@ other example apps in the same directory. __author__ = 'jcgregorio@google.com (Joe Gregorio)' -# Enable this sample to be run from the top-level directory -import os -import sys -sys.path.insert(0, os.getcwd()) - from apiclient.discovery import build from apiclient.oauth import FlowThreeLegged diff --git a/samples/cmdline/buzz.py b/samples/cmdline/buzz.py deleted file mode 100644 index f24c032..0000000 --- a/samples/cmdline/buzz.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python2.4 -# -*- coding: utf-8 -*- -# -# Copyright 2010 Google Inc. All Rights Reserved. - -"""Simple command-line example for Buzz. - -Command-line application that retrieves the users -latest content and then adds a new entry. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -# Enable this sample to be run from the top-level directory -import os -import sys -sys.path.insert(0, os.getcwd()) - -from apiclient.discovery import build - -import httplib2 -# httplib2.debuglevel = 4 -import pickle -import pprint - -def main(): - f = open("buzz.dat", "r") - credentials = pickle.loads(f.read()) - f.close() - - http = httplib2.Http() - http = credentials.authorize(http) - - p = build("buzz", "v1", http=http) - activities = p.activities() - activitylist = activities.list(max_results='2', scope='@self', userId='@me').execute() - print activitylist['items'][0]['title'] - activitylist = activities.list_next(activitylist).execute() - print activitylist['items'][0]['title'] - - activity = activities.insert(userId='@me', body={ - 'title': 'Testing insert', - 'object': { - 'content': u'Just a short note to show that insert is working. ☄', - 'type': 'note'} - } - ).execute() - pprint.pprint(activity) - print - print 'Just created: ', activity['links']['alternate'][0]['href'] - -if __name__ == '__main__': - main() diff --git a/samples/cmdline/moderator.py b/samples/cmdline/moderator.py deleted file mode 100644 index 36f354a..0000000 --- a/samples/cmdline/moderator.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python2.4 -# -*- coding: utf-8 -*- -# -# Copyright 2010 Google Inc. All Rights Reserved. - -"""Simple command-line example for Buzz. - -Command-line application that retrieves the users -latest content and then adds a new entry. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - - -from apiclient.discovery import build - -import httplib2 -import pickle - - -def main(): - f = open("moderator.dat", "r") - credentials = pickle.loads(f.read()) - f.close() - - http = httplib2.Http() - http = credentials.authorize(http) - - p = build("moderator", "v1", http=http) - print p.submissions().list(seriesId="7035", topicId="64").execute() - -if __name__ == '__main__': - main() diff --git a/samples/django_sample/__init__.py b/samples/django_sample/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/django_sample/buzz/__init__.py b/samples/django_sample/buzz/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/django_sample/buzz/models.py b/samples/django_sample/buzz/models.py new file mode 100644 index 0000000..91cdcce --- /dev/null +++ b/samples/django_sample/buzz/models.py @@ -0,0 +1,62 @@ +import pickle +import base64 + +import apiclient.oauth +from django.contrib import admin +from django.contrib.auth.models import User +from django.db import models + +# Create your models here. + +class OAuthCredentialsField(models.Field): + + __metaclass__ = models.SubfieldBase + + def db_type(self): + return 'VARCHAR' + + def to_python(self, value): + if value is None: + return None + if isinstance(value, apiclient.oauth.Credentials): + return value + return pickle.loads(base64.b64decode(value)) + + def get_db_prep_value(self, value): + return base64.b64encode(pickle.dumps(value)) + +class FlowThreeLeggedField(models.Field): + + __metaclass__ = models.SubfieldBase + + def db_type(self): + return 'VARCHAR' + + def to_python(self, value): + print "In to_python", value + if value is None: + return None + if isinstance(value, apiclient.oauth.FlowThreeLegged): + return value + return pickle.loads(base64.b64decode(value)) + + def get_db_prep_value(self, value): + return base64.b64encode(pickle.dumps(value)) + +# The Flow could also be stored in memcache since it is short lived. +class Flow(models.Model): + id = models.ForeignKey(User, primary_key=True) + flow = FlowThreeLeggedField() + +class Credential(models.Model): + id = models.ForeignKey(User, primary_key=True) + credential = OAuthCredentialsField() + +class CredentialAdmin(admin.ModelAdmin): + pass + +class FlowAdmin(admin.ModelAdmin): + pass + +admin.site.register(Credential, CredentialAdmin) +admin.site.register(Flow, FlowAdmin) diff --git a/samples/django_sample/buzz/tests.py b/samples/django_sample/buzz/tests.py new file mode 100644 index 0000000..2247054 --- /dev/null +++ b/samples/django_sample/buzz/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/samples/django_sample/buzz/views.py b/samples/django_sample/buzz/views.py new file mode 100644 index 0000000..aeb0ca2 --- /dev/null +++ b/samples/django_sample/buzz/views.py @@ -0,0 +1,62 @@ +import os +import logging +import httplib2 + +from django.http import HttpResponse +from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required +from django_sample.buzz.models import Credential, Flow +from apiclient.discovery import build +from apiclient.oauth import FlowThreeLegged +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response + +print os.environ +STEP2_URI = 'http://localhost:8000/auth_return' + +@login_required +def index(request): + try: + c = Credential.objects.get(id=request.user) + http = httplib2.Http() + http = c.credential.authorize(http) + p = build("buzz", "v1", http=http) + activities = p.activities() + activitylist = activities.list(scope='@consumption', + userId='@me').execute() + logging.info(activitylist) + + return render_to_response('buzz/welcome.html', { + 'activitylist': activitylist, + }) + + except Credential.DoesNotExist: + p = build("buzz", "v1") + flow = FlowThreeLegged(p.auth_discovery(), + consumer_key='anonymous', + consumer_secret='anonymous', + user_agent='google-api-client-python-buzz-django/1.0', + domain='anonymous', + scope='https://www.googleapis.com/auth/buzz', + xoauth_displayname='Django Example Web App') + + authorize_url = flow.step1_get_authorize_url(STEP2_URI) + f = Flow(id=request.user, flow=flow) + f.save() + return HttpResponseRedirect(authorize_url) + +@login_required +def auth_return(request): + try: + f = Flow.objects.get(id=request.user) + print f + print f.flow + print dir(f.flow) + print type(f.flow) + credential = f.flow.step2_exchange(request.REQUEST) + c = Credential(id=request.user, credential=credential) + c.save() + f.delete() + return HttpResponseRedirect("/") + except Flow.DoesNotExist: + pass diff --git a/samples/django_sample/manage.py b/samples/django_sample/manage.py new file mode 100755 index 0000000..bcdd55e --- /dev/null +++ b/samples/django_sample/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/samples/django_sample/settings.py b/samples/django_sample/settings.py new file mode 100644 index 0000000..834ce1f --- /dev/null +++ b/samples/django_sample/settings.py @@ -0,0 +1,83 @@ +# Django settings for django_sample project. +import os + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_NAME = 'database.sqlite3' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/New_York' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '_=9hq-$t_uv1ckf&s!y2$9g$1dm*6p1cl%*!^mg=7gr)!zj32d' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +) + +ROOT_URLCONF = 'django_sample.urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + os.path.join(os.path.dirname(__file__), 'templates') +) + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django_sample.buzz' +) diff --git a/samples/django_sample/static/go.png b/samples/django_sample/static/go.png new file mode 100644 index 0000000..e5aacda Binary files /dev/null and b/samples/django_sample/static/go.png differ diff --git a/samples/django_sample/templates/buzz/login.html b/samples/django_sample/templates/buzz/login.html new file mode 100644 index 0000000..567bf68 --- /dev/null +++ b/samples/django_sample/templates/buzz/login.html @@ -0,0 +1,23 @@ +{% block content %} + +{% if form.errors %} +

Your username and password didn't match. Please try again.

+{% endif %} + +
+ + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + + +
+ +{% endblock %} diff --git a/samples/django_sample/templates/buzz/welcome.html b/samples/django_sample/templates/buzz/welcome.html new file mode 100644 index 0000000..07e8027 --- /dev/null +++ b/samples/django_sample/templates/buzz/welcome.html @@ -0,0 +1,33 @@ + + + + Buzz Stuff + + + + + {% for item in activitylist.items %} + + + + + + {% endfor %} +
+ {% if item.actor.thumbnailUrl %} + + + + {% endif %} +
+ {{ item.actor.name }}
+ {{ item.object.content|safe }} + + +
+ + diff --git a/samples/django_sample/urls.py b/samples/django_sample/urls.py new file mode 100644 index 0000000..aeba620 --- /dev/null +++ b/samples/django_sample/urls.py @@ -0,0 +1,25 @@ +import os +from django.conf.urls.defaults import * + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + # Example: + (r'^$', 'django_sample.buzz.views.index'), + (r'^auth_return', 'django_sample.buzz.views.auth_return'), + + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # to INSTALLED_APPS to enable admin documentation: + # (r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + (r'^admin/', include(admin.site.urls)), + (r'^accounts/login/$', 'django.contrib.auth.views.login', + {'template_name': 'buzz/login.html'}), + + (r'^static/(?P.*)$', 'django.views.static.serve', + {'document_root': os.path.join(os.path.dirname(__file__), 'static') +}), +) diff --git a/samples/moderator/moderator.py b/samples/moderator/moderator.py new file mode 100644 index 0000000..3d742e8 --- /dev/null +++ b/samples/moderator/moderator.py @@ -0,0 +1,75 @@ +#!/usr/bin/python2.4 +# -*- coding: utf-8 -*- +# +# Copyright 2010 Google Inc. All Rights Reserved. + +"""Simple command-line example for Buzz. + +Command-line application that retrieves the users +latest content and then adds a new entry. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +from apiclient.discovery import build + +import httplib2 +import pickle + +# Uncomment to get detailed logging +# httplib2.debuglevel = 4 + +def main(): + f = open("moderator.dat", "r") + credentials = pickle.loads(f.read()) + f.close() + + http = httplib2.Http() + http = credentials.authorize(http) + + p = build("moderator", "v1", http=http) + + series_body = { + "description": "Share and rank tips for eating healthily on the cheaps!", + "name": "Eating Healthy & Cheap", + "videoSubmissionAllowed": False + } + series = p.series().insert(body=series_body).execute() + print "Created a new series" + + topic_body = { + "data": { + "description": "Share your ideas on eating healthy!", + "name": "Ideas", + "presenter": "liz" + } + } + topic = p.topics().insert(seriesId=series['id']['seriesId'], body=topic_body).execute() + print "Created a new topic" + + submission_body = { + "data": { + "attachmentUrl": "http://www.youtube.com/watch?v=1a1wyc5Xxpg", + "attribution": { + "displayName": "Bashan", + "location": "Bainbridge Island, WA" + }, + "text": "Charlie Ayers @ Google" + } + } + submission = p.submissions().insert(seriesId=topic['id']['seriesId'], + topicId=topic['id']['topicId'], body=submission_body).execute() + print "Inserted a new submisson on the topic" + + vote_body = { + "data": { + "vote": "PLUS" + } + } + p.votes().insert(seriesId=topic['id']['seriesId'], submissionId=submission['id']['submissionId'], body=vote_body) + print "Voted on the submission" + + +if __name__ == '__main__': + main() diff --git a/samples/cmdline/three_legged_dance_moderator.py b/samples/moderator/three_legged_dance.py similarity index 96% rename from samples/cmdline/three_legged_dance_moderator.py rename to samples/moderator/three_legged_dance.py index f09410d..fbc90ec 100644 --- a/samples/cmdline/three_legged_dance_moderator.py +++ b/samples/moderator/three_legged_dance.py @@ -35,6 +35,7 @@ flow = FlowThreeLegged(moderator_discovery, user_agent='google-api-client-python-mdrtr-cmdline/1.0', domain='anonymous', scope='https://www.googleapis.com/auth/moderator', + #scope='tag:google.com,2010:auth/moderator', xoauth_displayname='Google API Client Example App') authorize_url = flow.step1_get_authorize_url() diff --git a/tests/test_json_model.py b/tests/test_json_model.py index 79b63ab..01015f1 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -65,6 +65,22 @@ class Model(unittest.TestCase): self.assertNotEqual(query, '') self.assertEqual(body, '{"data": {}}') + def test_json_body_default_data(self): + """Test that a 'data' wrapper doesn't get added if one is already present.""" + model = JsonModel() + + headers = {} + path_params = {} + query_params = {} + body = {'data': 'foo'} + + headers, params, query, body = model.request(headers, path_params, query_params, body) + + self.assertEqual(headers['accept'], 'application/json') + self.assertEqual(headers['content-type'], 'application/json') + self.assertNotEqual(query, '') + self.assertEqual(body, '{"data": "foo"}') + def test_json_build_query(self): model = JsonModel() diff --git a/upload-diffs.py b/upload-diffs.py index 1b5daac..c3ff6b9 100644 --- a/upload-diffs.py +++ b/upload-diffs.py @@ -487,7 +487,7 @@ group.add_option("--rev", action="store", dest="revision", help="Base revision/branch/tree to diff against. Use " "rev1:rev2 range to review already committed changeset.") group.add_option("--send_mail", action="store_true", - dest="send_mail", default=False, + dest="send_mail", default=True, help="Send notification email to reviewers.") group.add_option("--vcs", action="store", dest="vcs", metavar="VCS", default=None,