blob: 825604a51234d9e00b2f943144ef6b6932054f60 [file] [log] [blame]
Joe Gregorio48d361f2010-08-18 13:19:21 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Client for discovery based APIs
16
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017A client library for Google's discovery based APIs.
Joe Gregorio48d361f2010-08-18 13:19:21 -040018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorioabda96f2011-02-11 20:19:33 -050021__all__ = [
22 'build', 'build_from_document'
23 ]
Joe Gregorio48d361f2010-08-18 13:19:21 -040024
25import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010026import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040027import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040028import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040029import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040030import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040031import urlparse
ade@google.comc5eb46f2010-09-27 23:35:39 +010032try:
33 from urlparse import parse_qsl
34except ImportError:
35 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050036
Joe Gregoriob843fa22010-12-13 16:26:07 -050037from http import HttpRequest
Joe Gregorio034e7002010-12-15 08:45:03 -050038from anyjson import simplejson
Joe Gregoriob843fa22010-12-13 16:26:07 -050039from model import JsonModel
Joe Gregoriob843fa22010-12-13 16:26:07 -050040from errors import UnknownLinkType
Joe Gregorioc0e0fe92011-03-04 16:16:55 -050041from errors import HttpError
Joe Gregorio49396552011-03-08 10:39:00 -050042from errors import InvalidJsonError
Joe Gregorio48d361f2010-08-18 13:19:21 -040043
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050044URITEMPLATE = re.compile('{[^}]*}')
45VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040046DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
47 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050048DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050049
50# Query parameters that work, but don't appear in discovery
Joe Gregorio06d852b2011-03-25 15:03:10 -040051STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
Joe Gregorio3eecaa92011-05-17 13:40:12 -040052 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040053
54
Joe Gregorio48d361f2010-08-18 13:19:21 -040055def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050056 """Converts key names into parameter names.
57
58 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040059 """
60 result = []
61 key = list(key)
62 if not key[0].isalpha():
63 result.append('x')
64 for c in key:
65 if c.isalnum():
66 result.append(c)
67 else:
68 result.append('_')
69
70 return ''.join(result)
71
72
Joe Gregorioaf276d22010-12-09 14:26:58 -050073def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050074 http=None,
75 discoveryServiceUrl=DISCOVERY_URI,
76 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050077 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -050078 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050079 """Construct a Resource for interacting with an API.
80
81 Construct a Resource object for interacting with
82 an API. The serviceName and version are the
83 names from the Discovery service.
84
85 Args:
86 serviceName: string, name of the service
87 version: string, the version of the service
88 discoveryServiceUrl: string, a URI Template that points to
89 the location of the discovery service. It should have two
90 parameters {api} and {apiVersion} that when filled in
91 produce an absolute URI to the discovery document for
92 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050093 developerKey: string, key obtained
94 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -050095 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -050096 requestBuilder: apiclient.http.HttpRequest, encapsulator for
97 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -050098
99 Returns:
100 A Resource object with methods for interacting with
101 the service.
102 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400103 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400104 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400105 'apiVersion': version
106 }
ade@google.com850cf552010-08-20 23:24:56 +0100107
Joe Gregorioc204b642010-09-21 12:01:23 -0400108 if http is None:
109 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100110 requested_url = uritemplate.expand(discoveryServiceUrl, params)
111 logging.info('URL being requested: %s' % requested_url)
112 resp, content = http.request(requested_url)
Joe Gregorio49396552011-03-08 10:39:00 -0500113 if resp.status > 400:
114 raise HttpError(resp, content, requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500115 try:
116 service = simplejson.loads(content)
117 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500118 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500119 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400120
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500121 fn = os.path.join(os.path.dirname(__file__), 'contrib',
122 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400123 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500124 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500125 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400126 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400127 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500128 future = None
129
130 return build_from_document(content, discoveryServiceUrl, future,
131 http, developerKey, model, requestBuilder)
132
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500133
Joe Gregorio292b9b82011-01-12 11:36:11 -0500134def build_from_document(
135 service,
136 base,
137 future=None,
138 http=None,
139 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500140 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500141 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500142 """Create a Resource for interacting with an API.
143
144 Same as `build()`, but constructs the Resource object
145 from a discovery document that is it given, as opposed to
146 retrieving one over HTTP.
147
Joe Gregorio292b9b82011-01-12 11:36:11 -0500148 Args:
149 service: string, discovery document
150 base: string, base URI for all HTTP requests, usually the discovery URI
151 future: string, discovery document with future capabilities
152 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500153 http: httplib2.Http, An instance of httplib2.Http or something that acts
154 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500155 developerKey: string, Key for controlling API usage, generated
156 from the API Console.
157 model: Model class instance that serializes and
158 de-serializes requests and responses.
159 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500160
161 Returns:
162 A Resource object with methods for interacting with
163 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500164 """
165
166 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400167 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500168 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500169 future = simplejson.loads(future)
170 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500171 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400172 future = {}
173 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400174
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500175 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500176 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500177 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500178 resource = createResource(http, base, model, requestBuilder, developerKey,
179 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400180
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500181 def auth_method():
182 """Discovery information about the authentication the API uses."""
183 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400184
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500185 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400186
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500187 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400188
189
Joe Gregorio61d7e962011-02-22 22:52:07 -0500190def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500191 """Convert value to a string based on JSON Schema type.
192
193 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
194 JSON Schema.
195
196 Args:
197 value: any, the value to convert
198 schema_type: string, the type that value should be interpreted as
199
200 Returns:
201 A string representation of 'value' based on the schema_type.
202 """
203 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500204 if type(value) == type('') or type(value) == type(u''):
205 return value
206 else:
207 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500208 elif schema_type == 'integer':
209 return str(int(value))
210 elif schema_type == 'number':
211 return str(float(value))
212 elif schema_type == 'boolean':
213 return str(bool(value)).lower()
214 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500215 if type(value) == type('') or type(value) == type(u''):
216 return value
217 else:
218 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500219
220
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500221def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500222 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400223
224 class Resource(object):
225 """A class for interacting with a resource."""
226
227 def __init__(self):
228 self._http = http
229 self._baseUrl = baseUrl
230 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400231 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500232 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400233
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400234 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio6a63a762011-05-02 22:36:05 -0400235 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400236 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400237 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400238
Joe Gregorioca876e42011-02-22 19:39:42 -0500239 if 'parameters' not in methodDesc:
240 methodDesc['parameters'] = {}
241 for name in STACK_QUERY_PARAMETERS:
242 methodDesc['parameters'][name] = {
243 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400244 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500245 }
246
Joe Gregoriof4153422011-03-18 22:45:18 -0400247 if httpMethod in ['PUT', 'POST', 'PATCH']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500248 methodDesc['parameters']['body'] = {
249 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500250 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500251 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500252 }
ade@google.com850cf552010-08-20 23:24:56 +0100253
Joe Gregorioca876e42011-02-22 19:39:42 -0500254 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100255 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500256 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100257 pattern_params = {} # Parameters that must match a regex
258 query_params = [] # Parameters that will be used in the query string
259 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500260 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500261 enum_params = {} # Allowable enumeration values for each parameter
262
263
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400264 if 'parameters' in methodDesc:
265 for arg, desc in methodDesc['parameters'].iteritems():
266 param = key2param(arg)
267 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400268
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400269 if desc.get('pattern', ''):
270 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500271 if desc.get('enum', ''):
272 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400273 if desc.get('required', False):
274 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500275 if desc.get('repeated', False):
276 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400277 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400278 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400279 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400280 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500281 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400282
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500283 for match in URITEMPLATE.finditer(pathUrl):
284 for namematch in VARNAME.finditer(match.group(0)):
285 name = key2param(namematch.group(0))
286 path_params[name] = name
287 if name in query_params:
288 query_params.remove(name)
289
Joe Gregorio48d361f2010-08-18 13:19:21 -0400290 def method(self, **kwargs):
291 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500292 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400293 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400294
ade@google.com850cf552010-08-20 23:24:56 +0100295 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400296 if name not in kwargs:
297 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400298
ade@google.com850cf552010-08-20 23:24:56 +0100299 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400300 if name in kwargs:
301 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400302 raise TypeError(
303 'Parameter "%s" value "%s" does not match the pattern "%s"' %
304 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400305
Joe Gregoriobee86832011-02-22 10:00:19 -0500306 for name, enums in enum_params.iteritems():
307 if name in kwargs:
308 if kwargs[name] not in enums:
309 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500310 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500311 (name, kwargs[name], str(enums)))
312
ade@google.com850cf552010-08-20 23:24:56 +0100313 actual_query_params = {}
314 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400315 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500316 to_type = param_type.get(key, 'string')
317 # For repeated parameters we cast each member of the list.
318 if key in repeated_params and type(value) == type([]):
319 cast_value = [_cast(x, to_type) for x in value]
320 else:
321 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100322 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500323 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100324 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500325 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100326 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400327
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400328 if self._developerKey:
329 actual_query_params['key'] = self._developerKey
330
Joe Gregorio48d361f2010-08-18 13:19:21 -0400331 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400332 headers, params, query, body = self._model.request(headers,
333 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400334
Joe Gregorioaf276d22010-12-09 14:26:58 -0500335 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
336 # document. Base URLs should not contain any path elements. If they do
337 # then urlparse.urljoin will strip them out This results in an incorrect
338 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100339 url_result = urlparse.urlsplit(self._baseUrl)
Joe Gregorio6f3014a2011-03-18 21:52:42 -0400340 new_base_url = url_result[0] + '://' + url_result[1]
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100341
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400342 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio6f3014a2011-03-18 21:52:42 -0400343 url = urlparse.urljoin(self._baseUrl,
344 url_result[2] + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400345
ade@google.com850cf552010-08-20 23:24:56 +0100346 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500347 return self._requestBuilder(self._http,
348 self._model.response,
349 url,
350 method=httpMethod,
351 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500352 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500353 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400354
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500355 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
356 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500357 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400358 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500359 if arg in STACK_QUERY_PARAMETERS:
360 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500361 repeated = ''
362 if arg in repeated_params:
363 repeated = ' (repeated)'
364 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400365 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500366 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500367 paramdesc = methodDesc['parameters'][argmap[arg]]
368 paramdoc = paramdesc.get('description', 'A parameter')
369 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500370 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
371 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500372 enum = paramdesc.get('enum', [])
373 enumDesc = paramdesc.get('enumDescriptions', [])
374 if enum and enumDesc:
375 docs.append(' Allowed values\n')
376 for (name, desc) in zip(enum, enumDesc):
377 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400378
379 setattr(method, '__doc__', ''.join(docs))
380 setattr(theclass, methodName, method)
381
Joe Gregorioaf276d22010-12-09 14:26:58 -0500382 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio6a63a762011-05-02 22:36:05 -0400383 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400384
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500385 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400386 """
387 Takes a single argument, 'body', which is the results
388 from the last call, and returns the next set of items
389 in the collection.
390
391 Returns None if there are no more items in
392 the collection.
393 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500394 if futureDesc['type'] != 'uri':
395 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400396
397 try:
398 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500399 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400400 p = p[key]
401 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400402 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400403 return None
404
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400405 if self._developerKey:
406 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100407 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400408 q.append(('key', self._developerKey))
409 parsed[4] = urllib.urlencode(q)
410 url = urlparse.urlunparse(parsed)
411
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400412 headers = {}
413 headers, params, query, body = self._model.request(headers, {}, {}, None)
414
415 logging.info('URL being requested: %s' % url)
416 resp, content = self._http.request(url, method='GET', headers=headers)
417
Joe Gregorioabda96f2011-02-11 20:19:33 -0500418 return self._requestBuilder(self._http,
419 self._model.response,
420 url,
421 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500422 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500423 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400424
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500425 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400426
427 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400428 if 'methods' in resourceDesc:
429 for methodName, methodDesc in resourceDesc['methods'].iteritems():
430 if futureDesc:
431 future = futureDesc['methods'].get(methodName, {})
432 else:
433 future = None
434 createMethod(Resource, methodName, methodDesc, future)
435
436 # Add in nested resources
437 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500438
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500439 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400440
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500441 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400442 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500443 self._requestBuilder, self._developerKey,
444 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400445
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500446 setattr(methodResource, '__doc__', 'A collection resource.')
447 setattr(methodResource, '__is_resource__', True)
448 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400449
450 for methodName, methodDesc in resourceDesc['resources'].iteritems():
451 if futureDesc and 'resources' in futureDesc:
452 future = futureDesc['resources'].get(methodName, {})
453 else:
454 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500455 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400456
457 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500458 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400459 for methodName, methodDesc in futureDesc['methods'].iteritems():
460 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500461 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500462 resourceDesc['methods'][methodName],
463 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400464
465 return Resource()