From 6d5e94f0ce4552832285cce22a8e3654b903dba3 Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Wed, 25 Aug 2010 23:49:30 -0400 Subject: [PATCH] First pass at adding in pagination --- Makefile | 3 + apiclient/contrib/buzz/future.json | 89 +++++++++++++++++++++++++ apiclient/contrib/latitude/future.json | 26 ++++++++ apiclient/contrib/moderator/future.json | 60 +++++++++++++++++ apiclient/discovery.py | 80 ++++++++++++++++++---- discovery_extras.py | 52 +++++++++++++++ samples/cmdline/three_legged_dance.py | 12 ++-- tests/data/buzz.json | 50 +++++++++----- tests/test_discovery.py | 15 ++++- tests/test_json_model.py | 18 ++++- 10 files changed, 366 insertions(+), 39 deletions(-) create mode 100644 apiclient/contrib/buzz/future.json create mode 100644 apiclient/contrib/latitude/future.json create mode 100644 apiclient/contrib/moderator/future.json create mode 100644 discovery_extras.py diff --git a/Makefile b/Makefile index debf9c3..12c6ff9 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,6 @@ pep8: test: python runtests.py + +skeletons: + python discovery_extras.py tests/data/buzz.json tests/data/latitude.json tests/data/moderator.json diff --git a/apiclient/contrib/buzz/future.json b/apiclient/contrib/buzz/future.json new file mode 100644 index 0000000..621ee52 --- /dev/null +++ b/apiclient/contrib/buzz/future.json @@ -0,0 +1,89 @@ +{ + "data": { + "buzz": { + "v1": { + "baseUrl": "https://www.googleapis.com/", + "resources": { + "activities": { + "methods": { + "delete": {}, + "get": {}, + "insert": {}, + "list": { + "next": { + "type": "uri", + "location": ["links", "next", 0, "href"] + } + }, + "search": { + "next": { + "type": "uri", + "location": ["links", "next", 0, "href"] + } + }, + "update": {} + } + }, + "comments": { + "methods": { + "delete": {}, + "get": {}, + "insert": {}, + "list": {}, + "update": {} + } + }, + "feeds": { + "methods": { + "delete": {}, + "insert": {}, + "list": {}, + "update": {} + } + }, + "groups": { + "methods": { + "delete": {}, + "get": {}, + "insert": {}, + "list": { + "next": { + "type": "uri", + "location": ["links", "next", 0, "href"] + } + }, + "update": {} + } + }, + "people": { + "methods": { + "delete": {}, + "get": {}, + "liked": {}, + "list": {}, + "relatedToUri": {}, + "reshared": {}, + "search": {}, + "update": {} + } + }, + "photos": { + "methods": { + "insert": {} + } + }, + "related": { + "methods": { + "list": {} + } + }, + "search": { + "methods": { + "extractPeople": {} + } + } + } + } + } + } +} diff --git a/apiclient/contrib/latitude/future.json b/apiclient/contrib/latitude/future.json new file mode 100644 index 0000000..657f6e8 --- /dev/null +++ b/apiclient/contrib/latitude/future.json @@ -0,0 +1,26 @@ +{ + "data": { + "latitude": { + "v1": { + "baseUrl": "https://www.googleapis.com/", + "resources": { + "currentLocation": { + "methods": { + "delete": {}, + "get": {}, + "insert": {} + } + }, + "location": { + "methods": { + "delete": {}, + "get": {}, + "insert": {}, + "list": {} + } + } + } + } + } + } +} \ No newline at end of file diff --git a/apiclient/contrib/moderator/future.json b/apiclient/contrib/moderator/future.json new file mode 100644 index 0000000..3d46f97 --- /dev/null +++ b/apiclient/contrib/moderator/future.json @@ -0,0 +1,60 @@ +{ + "data": { + "moderator": { + "v1": { + "baseUrl": "https://www.googleapis.com/", + "resources": { + "profiles": { + "methods": { + "get": {}, + "update": {} + } + }, + "responses": { + "methods": { + "insert": {}, + "list": {} + } + }, + "series": { + "methods": { + "get": {}, + "insert": {}, + "list": {}, + "update": {} + } + }, + "submissions": { + "methods": { + "get": {}, + "insert": {}, + "list": {} + } + }, + "tags": { + "methods": { + "delete": {}, + "insert": {}, + "list": {} + } + }, + "topics": { + "methods": { + "get": {}, + "insert": {}, + "list": {} + } + }, + "votes": { + "methods": { + "get": {}, + "insert": {}, + "list": {}, + "update": {} + } + } + } + } + } + } +} \ No newline at end of file diff --git a/apiclient/discovery.py b/apiclient/discovery.py index 3cd2c9f..d9fd085 100644 --- a/apiclient/discovery.py +++ b/apiclient/discovery.py @@ -23,17 +23,21 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import httplib2 import logging +import os import re import simplejson -import urlparse import uritemplate +import urlparse class HttpError(Exception): pass -DISCOVERY_URI = 'http://www.googleapis.com/discovery/0.1/describe\ -{?api,apiVersion}' +class UnknownLinkType(Exception): + pass + +DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe' + '{?api,apiVersion}') def key2method(key): @@ -107,10 +111,10 @@ class JsonModel(object): raise HttpError(simplejson.loads(content)['error']) -def build(service, version, http=httplib2.Http(), +def build(serviceName, version, http=httplib2.Http(), discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()): params = { - 'api': service, + 'api': serviceName, 'apiVersion': version } @@ -118,7 +122,14 @@ def build(service, version, http=httplib2.Http(), logging.info('URL being requested: %s' % requested_url) resp, content = http.request(requested_url) d = simplejson.loads(content) - service = d['data'][service][version] + service = d['data'][serviceName][version] + + fn = os.path.join(os.path.dirname(__file__), "contrib", serviceName, "future.json") + f = file(fn, "r") + d = simplejson.load(f) + f.close() + future = d['data'][serviceName][version]['resources'] + base = service['baseUrl'] resources = service['resources'] @@ -130,21 +141,21 @@ def build(service, version, http=httplib2.Http(), self._baseUrl = base self._model = model - def createMethod(theclass, methodName, methodDesc): + def createMethod(theclass, methodName, methodDesc, futureDesc): def method(self, **kwargs): return createResource(self._http, self._baseUrl, self._model, - methodName, methodDesc) + methodName, methodDesc, futureDesc) setattr(method, '__doc__', 'A description of how to use this function') setattr(theclass, methodName, method) for methodName, methodDesc in resources.iteritems(): - createMethod(Service, methodName, methodDesc) + createMethod(Service, methodName, methodDesc, future[methodName]) return Service() -def createResource(http, baseUrl, model, resourceName, resourceDesc): +def createResource(http, baseUrl, model, resourceName, resourceDesc, futureDesc): class Resource(object): """A class for interacting with a resource.""" @@ -154,7 +165,7 @@ def createResource(http, baseUrl, model, resourceName, resourceDesc): self._baseUrl = baseUrl self._model = model - def createMethod(theclass, methodName, methodDesc): + def createMethod(theclass, methodName, methodDesc, futureDesc): pathUrl = methodDesc['pathUrl'] pathUrl = re.sub(r'\{', r'{+', pathUrl) httpMethod = methodDesc['httpMethod'] @@ -207,7 +218,7 @@ def createResource(http, baseUrl, model, resourceName, resourceDesc): headers = {} headers, params, query, body = self._model.request(headers, actual_path_params, actual_query_params, body_value) - expanded_url = uritemplate.expand(pathUrl, params) + expanded_url = uritemplate.expand(pathUrl, params) url = urlparse.urljoin(self._baseUrl, expanded_url + query) logging.info('URL being requested: %s' % url) @@ -218,12 +229,53 @@ def createResource(http, baseUrl, model, resourceName, resourceDesc): docs = ['A description of how to use this function\n\n'] for arg in argmap.iterkeys(): - docs.append('%s - A parameter\n' % arg) + required = "" + if arg in required_params: + required = " (required)" + docs.append('%s - A parameter%s\n' % (arg, required)) setattr(method, '__doc__', ''.join(docs)) setattr(theclass, methodName, method) + def createNextMethod(theclass, methodName, methodDesc): + + def method(self, previous): + """ + Takes a single argument, 'body', which is the results + from the last call, and returns the next set of items + in the collection. + + Returns None if there are no more items in + the collection. + """ + if methodDesc['type'] != 'uri': + raise UnknownLinkType(methodDesc['type']) + + try: + p = previous + for key in methodDesc['location']: + p = p[key] + url = p + except KeyError: + return None + + headers = {} + headers, params, query, body = self._model.request(headers, {}, {}, None) + + logging.info('URL being requested: %s' % url) + resp, content = self._http.request(url, method='GET', headers=headers) + + return self._model.response(resp, content) + + setattr(theclass, methodName, method) + + # Add basic methods to Resource for methodName, methodDesc in resourceDesc['methods'].iteritems(): - createMethod(Resource, methodName, methodDesc) + createMethod(Resource, methodName, methodDesc, futureDesc['methods'].get(methodName, {})) + + # Add _next() methods to Resource + for methodName, methodDesc in futureDesc['methods'].iteritems(): + if 'next' in methodDesc and methodName in resourceDesc['methods']: + createNextMethod(Resource, methodName + "_next", methodDesc['next']) return Resource() diff --git a/discovery_extras.py b/discovery_extras.py new file mode 100644 index 0000000..f8c1d30 --- /dev/null +++ b/discovery_extras.py @@ -0,0 +1,52 @@ +# 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. + +"""Generate a skeleton discovery extras document. + +For the given API, retrieve the discovery document, +strip out the guts of each method description +and put : +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import os +import os.path +import simplejson +import sys + +def main(): + for filename in sys.argv[1:]: + f = file(filename, "r") + dis = simplejson.load(f) + f.close() + + data = dis['data'] + api = data[data.keys()[0]] + version = api[api.keys()[0]] + resources = version['resources'] + for res_name, res_desc in resources.iteritems(): + methods = res_desc['methods'] + for method_name, method_desc in methods.iteritems(): + methods[method_name] = {} + path, basename = os.path.split(filename) + newfilename = os.path.join(path, "skel-" + basename) + f = file(newfilename, "w") + simplejson.dump(dis, f, sort_keys=True, indent=2 * ' ') + f.close() + + +if __name__ == '__main__': + main() + diff --git a/samples/cmdline/three_legged_dance.py b/samples/cmdline/three_legged_dance.py index cc0ebf2..a6b1377 100644 --- a/samples/cmdline/three_legged_dance.py +++ b/samples/cmdline/three_legged_dance.py @@ -17,14 +17,14 @@ headers = {'user-agent': 'google-api-client-python-buzz-cmdline/1.0', consumer_key = 'anonymous' consumer_secret = 'anonymous' -request_token_url = 'https://www.google.com/accounts/OAuthGetRequestToken\ -?domain=anonymous&scope=https://www.googleapis.com/auth/buzz' +request_token_url = ('https://www.google.com/accounts/OAuthGetRequestToken' + '?domain=anonymous&scope=https://www.googleapis.com/auth/buzz') -access_token_url = 'https://www.google.com/accounts/OAuthGetAccessToken\ -?domain=anonymous&scope=https://www.googleapis.com/auth/buzz' +access_token_url = ('https://www.google.com/accounts/OAuthGetAccessToken' + '?domain=anonymous&scope=https://www.googleapis.com/auth/buzz') -authorize_url = 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken\ -?domain=anonymous&scope=https://www.googleapis.com/auth/buzz' +authorize_url = ('https://www.google.com/buzz/api/auth/OAuthAuthorizeToken' + '?domain=anonymous&scope=https://www.googleapis.com/auth/buzz') consumer = oauth.Consumer(consumer_key, consumer_secret) client = oauth.Client(consumer) diff --git a/tests/data/buzz.json b/tests/data/buzz.json index 0b25911..f12a445 100644 --- a/tests/data/buzz.json +++ b/tests/data/buzz.json @@ -497,6 +497,26 @@ } } }, + "relatedToUri": { + "pathUrl": "buzz/v1/people/{userId}/@related", + "rpcName": "buzz.people.relatedToUri", + "httpMethod": "POST", + "methodType": "rest", + "parameters": { + "alt": { + "parameterType": "query", + "required": false + }, + "uri": { + "parameterType": "query", + "required": false + }, + "hl": { + "parameterType": "query", + "required": false + } + } + }, "reshared": { "pathUrl": "buzz/v1/activities/{userId}/{scope}/{postId}/{groupId}", "rpcName": "buzz.people.reshared", @@ -722,15 +742,16 @@ } } }, - "insert": { - "pathUrl": "buzz/v1/people/{userId}/@groups", - "rpcName": "buzz.groups.insert", - "httpMethod": "POST", + "update": { + "pathUrl": "buzz/v1/people/{userId}/@groups/{groupId}/@self", + "rpcName": "buzz.groups.update", + "httpMethod": "PUT", "methodType": "rest", "parameters": { - "alt": { - "parameterType": "query", - "required": false + "groupId": { + "parameterType": "path", + "pattern": "[^/]+", + "required": true }, "userId": { "parameterType": "path", @@ -743,16 +764,15 @@ } } }, - "update": { - "pathUrl": "buzz/v1/people/{userId}/@groups/{groupId}/@self", - "rpcName": "buzz.groups.update", - "httpMethod": "PUT", + "insert": { + "pathUrl": "buzz/v1/people/{userId}/@groups", + "rpcName": "buzz.groups.insert", + "httpMethod": "POST", "methodType": "rest", "parameters": { - "groupId": { - "parameterType": "path", - "pattern": "[^/]+", - "required": true + "alt": { + "parameterType": "query", + "required": false }, "userId": { "parameterType": "path", diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 6af7d0b..539796f 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1,6 +1,19 @@ #!/usr/bin/python2.4 # -# Copyright 2010 Google Inc. All Rights Reserved. +# Copyright 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. + """Discovery document tests diff --git a/tests/test_json_model.py b/tests/test_json_model.py index 7b95d58..7af613f 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -1,10 +1,22 @@ #!/usr/bin/python2.4 # -# Copyright 2010 Google Inc. All Rights Reserved. +# Copyright 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. -"""Discovery document tests +"""JSON Model tests -Unit tests for objects created from discovery documents. +Unit tests for the JSON model. """ __author__ = 'jcgregorio@google.com (Joe Gregorio)'