From e48a491bea9122afcafcf26f34618757b763af07 Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Mon, 27 Sep 2010 15:05:36 -0400 Subject: [PATCH 01/12] First pass at a Django sample. --- .hgignore | 1 + samples/django_sample/__init__.py | 0 samples/django_sample/buzz/__init__.py | 0 samples/django_sample/buzz/models.py | 62 +++++++++++++ samples/django_sample/buzz/tests.py | 23 +++++ samples/django_sample/buzz/views.py | 62 +++++++++++++ samples/django_sample/manage.py | 11 +++ samples/django_sample/settings.py | 83 ++++++++++++++++++ samples/django_sample/static/go.png | Bin 0 -> 1139 bytes .../django_sample/templates/buzz/login.html | 23 +++++ .../django_sample/templates/buzz/welcome.html | 33 +++++++ samples/django_sample/urls.py | 25 ++++++ 12 files changed, 323 insertions(+) create mode 100644 samples/django_sample/__init__.py create mode 100644 samples/django_sample/buzz/__init__.py create mode 100644 samples/django_sample/buzz/models.py create mode 100644 samples/django_sample/buzz/tests.py create mode 100644 samples/django_sample/buzz/views.py create mode 100755 samples/django_sample/manage.py create mode 100644 samples/django_sample/settings.py create mode 100644 samples/django_sample/static/go.png create mode 100644 samples/django_sample/templates/buzz/login.html create mode 100644 samples/django_sample/templates/buzz/welcome.html create mode 100644 samples/django_sample/urls.py 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/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 0000000000000000000000000000000000000000..e5aacda8c654cc9c3349639d6975741c99a368c8 GIT binary patch literal 1139 zcmV-(1dRKMP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ige< z4j2fDH-&Qm000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}000BdNkl#C^jk*3sDS21QXOkMa4jn&22wE?$zCEyk<6g>N&gTedf%WGrNo?l}e?M z#l|4Zjg>OU@0#87Z(^C8$(r9724a|!Sg)c-rkf0AU{7}(l}8Rv9q%S zj^m)$>oG7efW5sv{M7%Mv;nxgyMxc?gUx0`PEL*_pO=>xEH5vkrKJT%qY)Dm6NpBm zbOO+7wQx8b2m}JSy}iZa;v%e8D>NF7B>$_cD@;yKqPn^oR;v|DOG|iuex|bq6c-m` zXlMwbPzcx8*YJA1Fq_Sid{e0u_V@QOI5-HsUJs7ru)VzvQ55Mcf)y1N@OV53hr>8K zJHz<+I1C1ZB;Q0L0iNg4(b0j*%1Vrmj^h0MoZeR0WHMoDY6{V46bA2#8O zW3d>#UN1~06B-&CFh4&}_gSE^u@Q4~bBM)a2nK`TI8O3Ta&&Zr;o)Ir9EMnyMORlB zR#sO2Y9~?DY4E>0NhA_Mb93|ePe3LCNG6jA27?d;0h^nfNF)-<2Ox?fc6WCn2m<_m zKc1eR{_PU{DdES*#}EVoYinzW$K$em>+9=52%!(a<>e&=L4Y6#h(sc?d`nA9!Eqc2 zAuyRt^khIh9>?nHDtMm9$;pW<-@Lp$^z`(=<#M5|tqlyr(6Iy_A0M&4z7C(yhfpXa zyNzvXY62kyeSLi>EG(p>1|$**1OfpFf`F~9E!m&L3_S`I|w1NPL3G~K&p6#VbI#z3PK2adwZePYJav9{Z}ulsi}d><$}}cM0t66 z#x!5w3ZST{2o8q>gb-LP7G$EO9RSO+=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') +}), +) From 567ee8d0fd1860ec6fba1827560d260a6b98d6a4 Mon Sep 17 00:00:00 2001 From: "ade@google.com" Date: Wed, 6 Oct 2010 06:13:18 -0700 Subject: [PATCH 02/12] Added tests verifying we can get follower and following counts as well as handling users with hidden follwer/following counts --- functional_tests/test_services.py | 34 +++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/functional_tests/test_services.py b/functional_tests/test_services.py index 6403946..e933094 100644 --- a/functional_tests/test_services.py +++ b/functional_tests/test_services.py @@ -86,6 +86,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): @@ -98,7 +123,6 @@ 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 buzz = build('buzz', 'v1', http=self.http) groups = buzz.groups().list(userId='108242092577082601423').execute() @@ -106,14 +130,6 @@ 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: - buzz = build('buzz', 'v1', http=self.http) - following = buzz.groups().get(userId='googlebuzz', groupId='@following').execute() - - self.assertEquals(17, len(following)) if __name__ == '__main__': unittest.main() From e2da544da802300f92f86f8d327ea8837e23e68a Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Wed, 6 Oct 2010 10:39:47 -0400 Subject: [PATCH 03/12] Change default to sending email for reviews. --- upload-diffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 05cd4f5fa24bfba1c6491c8dfa7d6a57ab96e5a1 Mon Sep 17 00:00:00 2001 From: Tom Miller Date: Wed, 6 Oct 2010 11:09:12 -0700 Subject: [PATCH 04/12] Add error classes for apiclient.discovery and apiclient.oauth --- apiclient/discovery.py | 12 ++++++++++-- apiclient/oauth.py | 16 +++++++++++++--- buzz_gae_client.py | 13 ++++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apiclient/discovery.py b/apiclient/discovery.py index 328d970..954ad6c 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}') diff --git a/apiclient/oauth.py b/apiclient/oauth.py index 8b827c6..2016dea 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 @@ -185,7 +195,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 +232,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): From e3c8b6d2fd457857f11f374bfae5987f09acf450 Mon Sep 17 00:00:00 2001 From: "jcgregorio@google.com" Date: Thu, 7 Oct 2010 19:34:54 -0400 Subject: [PATCH 05/12] Broke out the moderator and buzz samples into their own directories. Also added more functionality to the buzz sample. --- apiclient/discovery.py | 2 +- samples/buzz/buzz.py | 66 +++++++++++++++++++ .../{cmdline => buzz}/three_legged_dance.py | 5 -- samples/cmdline/buzz.py | 53 --------------- samples/{cmdline => moderator}/moderator.py | 0 .../three_legged_dance.py} | 0 6 files changed, 67 insertions(+), 59 deletions(-) create mode 100644 samples/buzz/buzz.py rename samples/{cmdline => buzz}/three_legged_dance.py (94%) delete mode 100644 samples/cmdline/buzz.py rename samples/{cmdline => moderator}/moderator.py (100%) rename samples/{cmdline/three_legged_dance_moderator.py => moderator/three_legged_dance.py} (100%) diff --git a/apiclient/discovery.py b/apiclient/discovery.py index 954ad6c..e71e990 100644 --- a/apiclient/discovery.py +++ b/apiclient/discovery.py @@ -99,7 +99,7 @@ class JsonModel(object): 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): 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/moderator/moderator.py similarity index 100% rename from samples/cmdline/moderator.py rename to samples/moderator/moderator.py diff --git a/samples/cmdline/three_legged_dance_moderator.py b/samples/moderator/three_legged_dance.py similarity index 100% rename from samples/cmdline/three_legged_dance_moderator.py rename to samples/moderator/three_legged_dance.py From 46b0ff63e7579ab4e8bf50a37a56818efad0e52e Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Sat, 9 Oct 2010 22:13:12 -0400 Subject: [PATCH 06/12] Expanded Moderator sample. Also made JsonBody more robust, allowing the external 'data' wrapper to be passed in from the caller or added, depending on whether it is present or not. Also handle adding user-agents better, by concatenating them. --- apiclient/contrib/moderator/future.json | 2 +- apiclient/discovery.py | 5 ++- apiclient/oauth.py | 7 ++-- samples/moderator/moderator.py | 44 ++++++++++++++++++++++++- samples/moderator/three_legged_dance.py | 1 + 5 files changed, 54 insertions(+), 5 deletions(-) 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 e71e990..db17cea 100644 --- a/apiclient/discovery.py +++ b/apiclient/discovery.py @@ -94,7 +94,10 @@ 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)) diff --git a/apiclient/oauth.py b/apiclient/oauth.py index 2016dea..de20336 100644 --- a/apiclient/oauth.py +++ b/apiclient/oauth.py @@ -130,8 +130,11 @@ 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'] += ' ' + else: + headers['user-agent'] = '' + headers['user-agent'] += self.user_agent return request_orig(uri, method, body, headers, redirections, connection_type) diff --git a/samples/moderator/moderator.py b/samples/moderator/moderator.py index 36f354a..3d742e8 100644 --- a/samples/moderator/moderator.py +++ b/samples/moderator/moderator.py @@ -17,6 +17,8 @@ from apiclient.discovery import build import httplib2 import pickle +# Uncomment to get detailed logging +# httplib2.debuglevel = 4 def main(): f = open("moderator.dat", "r") @@ -27,7 +29,47 @@ def main(): http = credentials.authorize(http) p = build("moderator", "v1", http=http) - print p.submissions().list(seriesId="7035", topicId="64").execute() + + 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/moderator/three_legged_dance.py b/samples/moderator/three_legged_dance.py index f09410d..fbc90ec 100644 --- a/samples/moderator/three_legged_dance.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() From 4f2794eecbc855bb26943c9e30e5bbaaec0a8369 Mon Sep 17 00:00:00 2001 From: "ade@google.com" Date: Mon, 11 Oct 2010 02:11:48 -0700 Subject: [PATCH 07/12] Added tests for getting a specific activity; creating a public activity; fetching specific groups for a user; commenting on an activity --- functional_tests/test_services.py | 92 ++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/functional_tests/test_services.py b/functional_tests/test_services.py index e933094..99efe35 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)' @@ -34,29 +35,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)) @@ -122,7 +137,19 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): self.http = credentials.authorize(httplib2.Http()) - def test_can_list_groups_belonging_to_user(self): + 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_identify_number_of_groups_belonging_to_user(self): buzz = build('buzz', 'v1', http=self.http) groups = buzz.groups().list(userId='108242092577082601423').execute() @@ -130,6 +157,57 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): expected_default_number_of_groups = 4 self.assertEquals(expected_default_number_of_groups, len(groups['items'])) + def IGNORE__test_can_like_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() + 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 + + 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) + if __name__ == '__main__': unittest.main() From 9d821b0559add4f90ec0ad5c08e4bf219b14bf29 Mon Sep 17 00:00:00 2001 From: "ade@google.com" Date: Mon, 11 Oct 2010 03:33:51 -0700 Subject: [PATCH 08/12] Added support for requests that return an HTTP 204: No Content. Added tests for creating a private activity and for deleting activities. --- apiclient/discovery.py | 3 +++ functional_tests/test_services.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/apiclient/discovery.py b/apiclient/discovery.py index db17cea..945db74 100644 --- a/apiclient/discovery.py +++ b/apiclient/discovery.py @@ -114,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/functional_tests/test_services.py b/functional_tests/test_services.py index 99efe35..50a1a4d 100644 --- a/functional_tests/test_services.py +++ b/functional_tests/test_services.py @@ -149,6 +149,23 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): ).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() @@ -208,6 +225,23 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): 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() + + 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() From 5e3a5fa791903672ff1e155c31909c00f824dba0 Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Mon, 11 Oct 2010 13:03:56 -0400 Subject: [PATCH 09/12] Sync httplib2. Fix user-agent code so base libraries are listed after higher level apps/libraries user-agent. --- apiclient/oauth.py | 5 ++--- httplib2/__init__.py | 10 +++++++--- httplib2/socks.py | 7 ++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apiclient/oauth.py b/apiclient/oauth.py index de20336..9907c46 100644 --- a/apiclient/oauth.py +++ b/apiclient/oauth.py @@ -131,10 +131,9 @@ class OAuthCredentials(Credentials): headers = {} headers.update(req.to_header()) if 'user-agent' in headers: - headers['user-agent'] += ' ' + headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] else: - headers['user-agent'] = '' - headers['user-agent'] += self.user_agent + headers['user-agent'] = self.user_agent return request_orig(uri, method, body, headers, redirections, connection_type) 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 From 8963ff928e88574db863b63d4ad6f0e2b13608d2 Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Mon, 11 Oct 2010 13:14:43 -0400 Subject: [PATCH 10/12] Added test of new model where 'data' wrapper isn't added if it isn't necessary. This will allow developers to cut and paste JSON from the developer docs into Python code and it should just work. --- functional_tests/test_services.py | 4 +++- tests/test_json_model.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/functional_tests/test_services.py b/functional_tests/test_services.py index 50a1a4d..a849577 100644 --- a/functional_tests/test_services.py +++ b/functional_tests/test_services.py @@ -19,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 @@ -227,7 +228,7 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): 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': { @@ -238,6 +239,7 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): 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') 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() From c0b63961f452293ca331dfe6c99be93b34938bac Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Mon, 11 Oct 2010 14:22:30 -0400 Subject: [PATCH 11/12] Renamed helpdoc to api-python-client-doc to make it easeier to find and associate with the appspot.com application. --- samples/{helpdoc => api-python-client-doc}/apiclient | 0 samples/{helpdoc => api-python-client-doc}/app.yaml | 0 samples/{helpdoc => api-python-client-doc}/httplib2 | 0 samples/{helpdoc => api-python-client-doc}/index.yaml | 0 samples/{helpdoc => api-python-client-doc}/main.py | 0 samples/{helpdoc => api-python-client-doc}/oauth2 | 0 samples/{helpdoc => api-python-client-doc}/simplejson | 0 samples/{helpdoc => api-python-client-doc}/uritemplate | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename samples/{helpdoc => api-python-client-doc}/apiclient (100%) rename samples/{helpdoc => api-python-client-doc}/app.yaml (100%) rename samples/{helpdoc => api-python-client-doc}/httplib2 (100%) rename samples/{helpdoc => api-python-client-doc}/index.yaml (100%) rename samples/{helpdoc => api-python-client-doc}/main.py (100%) rename samples/{helpdoc => api-python-client-doc}/oauth2 (100%) rename samples/{helpdoc => api-python-client-doc}/simplejson (100%) rename samples/{helpdoc => api-python-client-doc}/uritemplate (100%) 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 From a346c55371d7798f453c363e66e34fe25afa6233 Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Mon, 11 Oct 2010 14:26:48 -0400 Subject: [PATCH 12/12] Added README to api-python-client-doc --- samples/api-python-client-doc/README | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 samples/api-python-client-doc/README 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.