blob: 19af41a9b7699bad054477d95c62b3d9371ccf35 [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
Daniel Hermesc2113242013-02-27 10:16:13 -080015"""Client for discovery based APIs.
Joe Gregorio48d361f2010-08-18 13:19:21 -040016
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__ = [
Joe Gregorioce31a972012-06-06 15:48:17 -040022 'build',
Joe Gregorio1dc5dc82013-02-12 15:49:10 -050023 'build_from_document',
Joe Gregorioce31a972012-06-06 15:48:17 -040024 'fix_method_name',
Joe Gregorio1dc5dc82013-02-12 15:49:10 -050025 'key2param',
Joe Gregorioabda96f2011-02-11 20:19:33 -050026 ]
Joe Gregorio48d361f2010-08-18 13:19:21 -040027
Joe Gregorio3c676f92011-07-25 10:38:14 -040028import copy
Joe Gregorio48d361f2010-08-18 13:19:21 -040029import httplib2
Daniel Hermesc2113242013-02-27 10:16:13 -080030import keyword
ade@google.com850cf552010-08-20 23:24:56 +010031import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040032import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040033import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040034import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040035import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040036import urlparse
Joe Gregoriofdf7c802011-06-30 12:33:38 -040037import mimeparse
Joe Gregorio922b78c2011-05-26 21:36:34 -040038import mimetypes
39
ade@google.comc5eb46f2010-09-27 23:35:39 +010040try:
Joe Gregoriodc106fc2012-11-20 14:30:14 -050041 from urlparse import parse_qsl
ade@google.comc5eb46f2010-09-27 23:35:39 +010042except ImportError:
Joe Gregoriodc106fc2012-11-20 14:30:14 -050043 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050044
Joe Gregorio2b781282011-12-08 12:00:25 -050045from apiclient.errors import HttpError
46from apiclient.errors import InvalidJsonError
47from apiclient.errors import MediaUploadSizeError
48from apiclient.errors import UnacceptableMimeTypeError
49from apiclient.errors import UnknownApiNameOrVersion
Joe Gregoriodc106fc2012-11-20 14:30:14 -050050from apiclient.errors import UnknownFileType
Joe Gregorio2b781282011-12-08 12:00:25 -050051from apiclient.http import HttpRequest
52from apiclient.http import MediaFileUpload
53from apiclient.http import MediaUpload
54from apiclient.model import JsonModel
Joe Gregorio708388c2012-06-15 13:43:04 -040055from apiclient.model import MediaModel
Joe Gregorio2b781282011-12-08 12:00:25 -050056from apiclient.model import RawModel
57from apiclient.schema import Schemas
Joe Gregorio922b78c2011-05-26 21:36:34 -040058from email.mime.multipart import MIMEMultipart
59from email.mime.nonmultipart import MIMENonMultipart
Joe Gregoriof4839b02012-09-06 13:47:24 -040060from oauth2client.util import positional
Joe Gregorio10244032013-03-06 09:48:04 -050061from oauth2client.util import _add_query_parameter
Joe Gregorio549230c2012-01-11 10:38:05 -050062from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050063
Joe Gregorio504a17f2012-12-07 14:14:26 -050064# The client library requires a version of httplib2 that supports RETRIES.
65httplib2.RETRIES = 1
66
Joe Gregorioe84c9442012-03-12 08:45:57 -040067logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040068
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050069URITEMPLATE = re.compile('{[^}]*}')
70VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040071DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
Daniel Hermesc2113242013-02-27 10:16:13 -080072 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050073DEFAULT_METHOD_DOC = 'A description of how to use this function'
Daniel Hermesc2113242013-02-27 10:16:13 -080074HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH'])
75_MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40}
76BODY_PARAMETER_DEFAULT_VALUE = {
77 'description': 'The request body.',
78 'type': 'object',
79 'required': True,
80}
81MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
82 'description': ('The filename of the media request body, or an instance '
83 'of a MediaUpload object.'),
84 'type': 'string',
85 'required': False,
86}
Joe Gregorioca876e42011-02-22 19:39:42 -050087
Joe Gregorioc8e421c2012-06-06 14:03:13 -040088# Parameters accepted by the stack, but not visible via discovery.
Daniel Hermesc2113242013-02-27 10:16:13 -080089# TODO(dhermes): Remove 'userip' in 'v2'.
90STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict'])
91STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'}
Joe Gregorio48d361f2010-08-18 13:19:21 -040092
Daniel Hermesc2113242013-02-27 10:16:13 -080093# Library-specific reserved words beyond Python keywords.
94RESERVED_WORDS = frozenset(['body'])
Joe Gregoriod92897c2011-07-07 11:44:56 -040095
Joe Gregorio562b7312011-09-15 09:06:38 -040096
Joe Gregorioce31a972012-06-06 15:48:17 -040097def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040098 """Fix method names to avoid reserved word conflicts.
99
100 Args:
101 name: string, method name.
102
103 Returns:
104 The name with a '_' prefixed if the name is a reserved word.
105 """
Daniel Hermesc2113242013-02-27 10:16:13 -0800106 if keyword.iskeyword(name) or name in RESERVED_WORDS:
Joe Gregoriod92897c2011-07-07 11:44:56 -0400107 return name + '_'
108 else:
109 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -0400110
Joe Gregorioa98733f2011-09-16 10:12:28 -0400111
Joe Gregorio48d361f2010-08-18 13:19:21 -0400112def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500113 """Converts key names into parameter names.
114
115 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400116
117 Args:
118 key: string, the method key name.
119
120 Returns:
121 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400122 """
123 result = []
124 key = list(key)
125 if not key[0].isalpha():
126 result.append('x')
127 for c in key:
128 if c.isalnum():
129 result.append(c)
130 else:
131 result.append('_')
132
133 return ''.join(result)
134
135
Joe Gregoriof4839b02012-09-06 13:47:24 -0400136@positional(2)
Joe Gregorio01770a52012-02-24 11:11:10 -0500137def build(serviceName,
138 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500139 http=None,
140 discoveryServiceUrl=DISCOVERY_URI,
141 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500142 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500143 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500144 """Construct a Resource for interacting with an API.
145
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400146 Construct a Resource object for interacting with an API. The serviceName and
147 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500148
149 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400150 serviceName: string, name of the service.
151 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500152 http: httplib2.Http, An instance of httplib2.Http or something that acts
153 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400154 discoveryServiceUrl: string, a URI Template that points to the location of
155 the discovery service. It should have two parameters {api} and
156 {apiVersion} that when filled in produce an absolute URI to the discovery
157 document for that service.
158 developerKey: string, key obtained from
159 https://code.google.com/apis/console.
160 model: apiclient.Model, converts to and from the wire format.
161 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
162 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500163
164 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400165 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500166 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400167 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400168 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400169 'apiVersion': version
170 }
ade@google.com850cf552010-08-20 23:24:56 +0100171
Joe Gregorioc204b642010-09-21 12:01:23 -0400172 if http is None:
173 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400174
ade@google.com850cf552010-08-20 23:24:56 +0100175 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400176
Joe Gregorio66f57522011-11-30 11:00:00 -0500177 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
178 # variable that contains the network address of the client sending the
179 # request. If it exists then add that to the request for the discovery
180 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400181 if 'REMOTE_ADDR' in os.environ:
182 requested_url = _add_query_parameter(requested_url, 'userIp',
183 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400184 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400185
ade@google.com850cf552010-08-20 23:24:56 +0100186 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400187
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500188 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500189 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500190 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400191 if resp.status >= 400:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400192 raise HttpError(resp, content, uri=requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400193
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500194 try:
195 service = simplejson.loads(content)
196 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400197 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500198 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400199
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400200 return build_from_document(content, base=discoveryServiceUrl, http=http,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400201 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500202
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500203
Joe Gregoriof4839b02012-09-06 13:47:24 -0400204@positional(1)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500205def build_from_document(
206 service,
Joe Gregorioa2838152012-07-16 11:52:17 -0400207 base=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500208 future=None,
209 http=None,
210 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500211 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500212 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500213 """Create a Resource for interacting with an API.
214
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400215 Same as `build()`, but constructs the Resource object from a discovery
216 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500217
Joe Gregorio292b9b82011-01-12 11:36:11 -0500218 Args:
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500219 service: string or object, the JSON discovery document describing the API.
220 The value passed in may either be the JSON string or the deserialized
221 JSON.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400222 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400223 This parameter is no longer used as rootUrl and servicePath are included
224 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400225 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500226 http: httplib2.Http, An instance of httplib2.Http or something that acts
227 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500228 developerKey: string, Key for controlling API usage, generated
229 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400230 model: Model class instance that serializes and de-serializes requests and
231 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500232 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500233
234 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400235 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500236 """
237
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400238 # future is no longer used.
239 future = {}
240
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500241 if isinstance(service, basestring):
242 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400243 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500244 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400245
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500246 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500247 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500248 model = JsonModel('dataWrapper' in features)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500249 return Resource(http=http, baseUrl=base, model=model,
250 developerKey=developerKey, requestBuilder=requestBuilder,
251 resourceDesc=service, rootDesc=service, schema=schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400252
253
Joe Gregorio61d7e962011-02-22 22:52:07 -0500254def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500255 """Convert value to a string based on JSON Schema type.
256
257 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
258 JSON Schema.
259
260 Args:
261 value: any, the value to convert
262 schema_type: string, the type that value should be interpreted as
263
264 Returns:
265 A string representation of 'value' based on the schema_type.
266 """
267 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500268 if type(value) == type('') or type(value) == type(u''):
269 return value
270 else:
271 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500272 elif schema_type == 'integer':
273 return str(int(value))
274 elif schema_type == 'number':
275 return str(float(value))
276 elif schema_type == 'boolean':
277 return str(bool(value)).lower()
278 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500279 if type(value) == type('') or type(value) == type(u''):
280 return value
281 else:
282 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500283
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400284
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400285def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400286 """Convert a string media size, such as 10GB or 3TB into an integer.
287
288 Args:
289 maxSize: string, size as a string, such as 2MB or 7GB.
290
291 Returns:
292 The size as an integer value.
293 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400294 if len(maxSize) < 2:
Daniel Hermesc2113242013-02-27 10:16:13 -0800295 return 0L
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400296 units = maxSize[-2:].upper()
Daniel Hermesc2113242013-02-27 10:16:13 -0800297 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
298 if bit_shift is not None:
299 return long(maxSize[:-2]) << bit_shift
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400300 else:
Daniel Hermesc2113242013-02-27 10:16:13 -0800301 return long(maxSize)
302
303
304def _media_path_url_from_info(root_desc, path_url):
305 """Creates an absolute media path URL.
306
307 Constructed using the API root URI and service path from the discovery
308 document and the relative path for the API method.
309
310 Args:
311 root_desc: Dictionary; the entire original deserialized discovery document.
312 path_url: String; the relative URL for the API method. Relative to the API
313 root, which is specified in the discovery document.
314
315 Returns:
316 String; the absolute URI for media upload for the API method.
317 """
318 return '%(root)supload/%(service_path)s%(path)s' % {
319 'root': root_desc['rootUrl'],
320 'service_path': root_desc['servicePath'],
321 'path': path_url,
322 }
323
324
325def _fix_up_parameters(method_desc, root_desc, http_method):
326 """Updates parameters of an API method with values specific to this library.
327
328 Specifically, adds whatever global parameters are specified by the API to the
329 parameters for the individual method. Also adds parameters which don't
330 appear in the discovery document, but are available to all discovery based
331 APIs (these are listed in STACK_QUERY_PARAMETERS).
332
333 SIDE EFFECTS: This updates the parameters dictionary object in the method
334 description.
335
336 Args:
337 method_desc: Dictionary with metadata describing an API method. Value comes
338 from the dictionary of methods stored in the 'methods' key in the
339 deserialized discovery document.
340 root_desc: Dictionary; the entire original deserialized discovery document.
341 http_method: String; the HTTP method used to call the API method described
342 in method_desc.
343
344 Returns:
345 The updated Dictionary stored in the 'parameters' key of the method
346 description dictionary.
347 """
348 parameters = method_desc.setdefault('parameters', {})
349
350 # Add in the parameters common to all methods.
351 for name, description in root_desc.get('parameters', {}).iteritems():
352 parameters[name] = description
353
354 # Add in undocumented query parameters.
355 for name in STACK_QUERY_PARAMETERS:
356 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
357
358 # Add 'body' (our own reserved word) to parameters if the method supports
359 # a request payload.
360 if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc:
361 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
362 body.update(method_desc['request'])
363 parameters['body'] = body
364
365 return parameters
366
367
368def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
369 """Updates parameters of API by adding 'media_body' if supported by method.
370
371 SIDE EFFECTS: If the method supports media upload and has a required body,
372 sets body to be optional (required=False) instead. Also, if there is a
373 'mediaUpload' in the method description, adds 'media_upload' key to
374 parameters.
375
376 Args:
377 method_desc: Dictionary with metadata describing an API method. Value comes
378 from the dictionary of methods stored in the 'methods' key in the
379 deserialized discovery document.
380 root_desc: Dictionary; the entire original deserialized discovery document.
381 path_url: String; the relative URL for the API method. Relative to the API
382 root, which is specified in the discovery document.
383 parameters: A dictionary describing method parameters for method described
384 in method_desc.
385
386 Returns:
387 Triple (accept, max_size, media_path_url) where:
388 - accept is a list of strings representing what content types are
389 accepted for media upload. Defaults to empty list if not in the
390 discovery document.
391 - max_size is a long representing the max size in bytes allowed for a
392 media upload. Defaults to 0L if not in the discovery document.
393 - media_path_url is a String; the absolute URI for media upload for the
394 API method. Constructed using the API root URI and service path from
395 the discovery document and the relative path for the API method. If
396 media upload is not supported, this is None.
397 """
398 media_upload = method_desc.get('mediaUpload', {})
399 accept = media_upload.get('accept', [])
400 max_size = _media_size_to_long(media_upload.get('maxSize', ''))
401 media_path_url = None
402
403 if media_upload:
404 media_path_url = _media_path_url_from_info(root_desc, path_url)
405 parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
406 if 'body' in parameters:
407 parameters['body']['required'] = False
408
409 return accept, max_size, media_path_url
410
411
412def _fix_up_method_description(method_desc, root_desc):
413 """Updates a method description in a discovery document.
414
415 SIDE EFFECTS: Changes the parameters dictionary in the method description with
416 extra parameters which are used locally.
417
418 Args:
419 method_desc: Dictionary with metadata describing an API method. Value comes
420 from the dictionary of methods stored in the 'methods' key in the
421 deserialized discovery document.
422 root_desc: Dictionary; the entire original deserialized discovery document.
423
424 Returns:
425 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
426 where:
427 - path_url is a String; the relative URL for the API method. Relative to
428 the API root, which is specified in the discovery document.
429 - http_method is a String; the HTTP method used to call the API method
430 described in the method description.
431 - method_id is a String; the name of the RPC method associated with the
432 API method, and is in the method description in the 'id' key.
433 - accept is a list of strings representing what content types are
434 accepted for media upload. Defaults to empty list if not in the
435 discovery document.
436 - max_size is a long representing the max size in bytes allowed for a
437 media upload. Defaults to 0L if not in the discovery document.
438 - media_path_url is a String; the absolute URI for media upload for the
439 API method. Constructed using the API root URI and service path from
440 the discovery document and the relative path for the API method. If
441 media upload is not supported, this is None.
442 """
443 path_url = method_desc['path']
444 http_method = method_desc['httpMethod']
445 method_id = method_desc['id']
446
447 parameters = _fix_up_parameters(method_desc, root_desc, http_method)
448 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
449 # 'parameters' key and needs to know if there is a 'body' parameter because it
450 # also sets a 'media_body' parameter.
451 accept, max_size, media_path_url = _fix_up_media_upload(
452 method_desc, root_desc, path_url, parameters)
453
454 return path_url, http_method, method_id, accept, max_size, media_path_url
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400455
Joe Gregoriobee86832011-02-22 10:00:19 -0500456
Daniel Hermes954e1242013-02-28 09:28:37 -0800457# TODO(dhermes): Convert this class to ResourceMethod and make it callable
458class ResourceMethodParameters(object):
459 """Represents the parameters associated with a method.
460
461 Attributes:
462 argmap: Map from method parameter name (string) to query parameter name
463 (string).
464 required_params: List of required parameters (represented by parameter
465 name as string).
466 repeated_params: List of repeated parameters (represented by parameter
467 name as string).
468 pattern_params: Map from method parameter name (string) to regular
469 expression (as a string). If the pattern is set for a parameter, the
470 value for that parameter must match the regular expression.
471 query_params: List of parameters (represented by parameter name as string)
472 that will be used in the query string.
473 path_params: Set of parameters (represented by parameter name as string)
474 that will be used in the base URL path.
475 param_types: Map from method parameter name (string) to parameter type. Type
476 can be any valid JSON schema type; valid values are 'any', 'array',
477 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
478 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
479 enum_params: Map from method parameter name (string) to list of strings,
480 where each list of strings is the list of acceptable enum values.
481 """
482
483 def __init__(self, method_desc):
484 """Constructor for ResourceMethodParameters.
485
486 Sets default values and defers to set_parameters to populate.
487
488 Args:
489 method_desc: Dictionary with metadata describing an API method. Value
490 comes from the dictionary of methods stored in the 'methods' key in
491 the deserialized discovery document.
492 """
493 self.argmap = {}
494 self.required_params = []
495 self.repeated_params = []
496 self.pattern_params = {}
497 self.query_params = []
498 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
499 # parsing is gotten rid of.
500 self.path_params = set()
501 self.param_types = {}
502 self.enum_params = {}
503
504 self.set_parameters(method_desc)
505
506 def set_parameters(self, method_desc):
507 """Populates maps and lists based on method description.
508
509 Iterates through each parameter for the method and parses the values from
510 the parameter dictionary.
511
512 Args:
513 method_desc: Dictionary with metadata describing an API method. Value
514 comes from the dictionary of methods stored in the 'methods' key in
515 the deserialized discovery document.
516 """
517 for arg, desc in method_desc.get('parameters', {}).iteritems():
518 param = key2param(arg)
519 self.argmap[param] = arg
520
521 if desc.get('pattern'):
522 self.pattern_params[param] = desc['pattern']
523 if desc.get('enum'):
524 self.enum_params[param] = desc['enum']
525 if desc.get('required'):
526 self.required_params.append(param)
527 if desc.get('repeated'):
528 self.repeated_params.append(param)
529 if desc.get('location') == 'query':
530 self.query_params.append(param)
531 if desc.get('location') == 'path':
532 self.path_params.add(param)
533 self.param_types[param] = desc.get('type', 'string')
534
535 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
536 # should have all path parameters already marked with
537 # 'location: path'.
538 for match in URITEMPLATE.finditer(method_desc['path']):
539 for namematch in VARNAME.finditer(match.group(0)):
540 name = key2param(namematch.group(0))
541 self.path_params.add(name)
542 if name in self.query_params:
543 self.query_params.remove(name)
544
545
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500546def createMethod(methodName, methodDesc, rootDesc, schema):
547 """Creates a method for attaching to a Resource.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400548
549 Args:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500550 methodName: string, name of the method to use.
551 methodDesc: object, fragment of deserialized discovery document that
552 describes the method.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400553 rootDesc: object, the entire deserialized discovery document.
554 schema: object, mapping of schema names to schema descriptions.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400555 """
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500556 methodName = fix_method_name(methodName)
Daniel Hermesc2113242013-02-27 10:16:13 -0800557 (pathUrl, httpMethod, methodId, accept,
558 maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400559
Daniel Hermes954e1242013-02-28 09:28:37 -0800560 parameters = ResourceMethodParameters(methodDesc)
ade@google.com850cf552010-08-20 23:24:56 +0100561
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500562 def method(self, **kwargs):
563 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorioca876e42011-02-22 19:39:42 -0500564
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500565 for name in kwargs.iterkeys():
Daniel Hermes954e1242013-02-28 09:28:37 -0800566 if name not in parameters.argmap:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500567 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorioca876e42011-02-22 19:39:42 -0500568
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500569 # Remove args that have a value of None.
570 keys = kwargs.keys()
571 for name in keys:
572 if kwargs[name] is None:
573 del kwargs[name]
Joe Gregorio21f11672010-08-18 17:23:17 -0400574
Daniel Hermes954e1242013-02-28 09:28:37 -0800575 for name in parameters.required_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500576 if name not in kwargs:
577 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400578
Daniel Hermes954e1242013-02-28 09:28:37 -0800579 for name, regex in parameters.pattern_params.iteritems():
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500580 if name in kwargs:
581 if isinstance(kwargs[name], basestring):
582 pvalues = [kwargs[name]]
Joe Gregorio61d7e962011-02-22 22:52:07 -0500583 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500584 pvalues = kwargs[name]
585 for pvalue in pvalues:
586 if re.match(regex, pvalue) is None:
587 raise TypeError(
588 'Parameter "%s" value "%s" does not match the pattern "%s"' %
589 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400590
Daniel Hermes954e1242013-02-28 09:28:37 -0800591 for name, enums in parameters.enum_params.iteritems():
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500592 if name in kwargs:
593 # We need to handle the case of a repeated enum
594 # name differently, since we want to handle both
595 # arg='value' and arg=['value1', 'value2']
Daniel Hermes954e1242013-02-28 09:28:37 -0800596 if (name in parameters.repeated_params and
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500597 not isinstance(kwargs[name], basestring)):
598 values = kwargs[name]
599 else:
600 values = [kwargs[name]]
601 for value in values:
602 if value not in enums:
603 raise TypeError(
604 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
605 (name, value, str(enums)))
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400606
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500607 actual_query_params = {}
608 actual_path_params = {}
609 for key, value in kwargs.iteritems():
Daniel Hermes954e1242013-02-28 09:28:37 -0800610 to_type = parameters.param_types.get(key, 'string')
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500611 # For repeated parameters we cast each member of the list.
Daniel Hermes954e1242013-02-28 09:28:37 -0800612 if key in parameters.repeated_params and type(value) == type([]):
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500613 cast_value = [_cast(x, to_type) for x in value]
614 else:
615 cast_value = _cast(value, to_type)
Daniel Hermes954e1242013-02-28 09:28:37 -0800616 if key in parameters.query_params:
617 actual_query_params[parameters.argmap[key]] = cast_value
618 if key in parameters.path_params:
619 actual_path_params[parameters.argmap[key]] = cast_value
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500620 body_value = kwargs.get('body', None)
621 media_filename = kwargs.get('media_body', None)
Joe Gregorioe08a1662011-12-07 09:48:22 -0500622
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500623 if self._developerKey:
624 actual_query_params['key'] = self._developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400625
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500626 model = self._model
627 if methodName.endswith('_media'):
628 model = MediaModel()
629 elif 'response' not in methodDesc:
630 model = RawModel()
631
632 headers = {}
633 headers, params, query, body = model.request(headers,
634 actual_path_params, actual_query_params, body_value)
635
636 expanded_url = uritemplate.expand(pathUrl, params)
637 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
638
639 resumable = None
640 multipart_boundary = ''
641
642 if media_filename:
643 # Ensure we end up with a valid MediaUpload object.
644 if isinstance(media_filename, basestring):
645 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
646 if media_mime_type is None:
647 raise UnknownFileType(media_filename)
648 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
649 raise UnacceptableMimeTypeError(media_mime_type)
650 media_upload = MediaFileUpload(media_filename,
651 mimetype=media_mime_type)
652 elif isinstance(media_filename, MediaUpload):
653 media_upload = media_filename
654 else:
655 raise TypeError('media_filename must be str or MediaUpload.')
656
657 # Check the maxSize
658 if maxSize > 0 and media_upload.size() > maxSize:
659 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
660
661 # Use the media path uri for media uploads
662 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400663 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500664 if media_upload.resumable():
665 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400666
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500667 if media_upload.resumable():
668 # This is all we need to do for resumable, if the body exists it gets
669 # sent in the first request, otherwise an empty body is sent.
670 resumable = media_upload
Joe Gregorio2b781282011-12-08 12:00:25 -0500671 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500672 # A non-resumable upload
673 if body is None:
674 # This is a simple media upload
675 headers['content-type'] = media_upload.mimetype()
676 body = media_upload.getbytes(0, media_upload.size())
677 url = _add_query_parameter(url, 'uploadType', 'media')
678 else:
679 # This is a multipart/related upload.
680 msgRoot = MIMEMultipart('related')
681 # msgRoot should not write out it's own headers
682 setattr(msgRoot, '_write_headers', lambda self: None)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400683
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500684 # attach the body as one part
685 msg = MIMENonMultipart(*headers['content-type'].split('/'))
686 msg.set_payload(body)
687 msgRoot.attach(msg)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400688
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500689 # attach the media as the second part
690 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
691 msg['Content-Transfer-Encoding'] = 'binary'
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400692
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500693 payload = media_upload.getbytes(0, media_upload.size())
694 msg.set_payload(payload)
695 msgRoot.attach(msg)
696 body = msgRoot.as_string()
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400697
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500698 multipart_boundary = msgRoot.get_boundary()
699 headers['content-type'] = ('multipart/related; '
700 'boundary="%s"') % multipart_boundary
701 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400702
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500703 logger.info('URL being requested: %s' % url)
704 return self._requestBuilder(self._http,
705 model.response,
706 url,
707 method=httpMethod,
708 body=body,
709 headers=headers,
710 methodId=methodId,
711 resumable=resumable)
712
713 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
Daniel Hermes954e1242013-02-28 09:28:37 -0800714 if len(parameters.argmap) > 0:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500715 docs.append('Args:\n')
716
717 # Skip undocumented params and params common to all methods.
718 skip_parameters = rootDesc.get('parameters', {}).keys()
719 skip_parameters.extend(STACK_QUERY_PARAMETERS)
720
Daniel Hermes954e1242013-02-28 09:28:37 -0800721 all_args = parameters.argmap.keys()
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500722 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
723
724 # Move body to the front of the line.
725 if 'body' in all_args:
726 args_ordered.append('body')
727
728 for name in all_args:
729 if name not in args_ordered:
730 args_ordered.append(name)
731
732 for arg in args_ordered:
733 if arg in skip_parameters:
734 continue
735
736 repeated = ''
Daniel Hermes954e1242013-02-28 09:28:37 -0800737 if arg in parameters.repeated_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500738 repeated = ' (repeated)'
739 required = ''
Daniel Hermes954e1242013-02-28 09:28:37 -0800740 if arg in parameters.required_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500741 required = ' (required)'
Daniel Hermes954e1242013-02-28 09:28:37 -0800742 paramdesc = methodDesc['parameters'][parameters.argmap[arg]]
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500743 paramdoc = paramdesc.get('description', 'A parameter')
744 if '$ref' in paramdesc:
745 docs.append(
746 (' %s: object, %s%s%s\n The object takes the'
747 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
748 schema.prettyPrintByName(paramdesc['$ref'])))
749 else:
750 paramtype = paramdesc.get('type', 'string')
751 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
752 repeated))
753 enum = paramdesc.get('enum', [])
754 enumDesc = paramdesc.get('enumDescriptions', [])
755 if enum and enumDesc:
756 docs.append(' Allowed values\n')
757 for (name, desc) in zip(enum, enumDesc):
758 docs.append(' %s - %s\n' % (name, desc))
759 if 'response' in methodDesc:
760 if methodName.endswith('_media'):
761 docs.append('\nReturns:\n The media object as a string.\n\n ')
762 else:
763 docs.append('\nReturns:\n An object of the form:\n\n ')
764 docs.append(schema.prettyPrintSchema(methodDesc['response']))
765
766 setattr(method, '__doc__', ''.join(docs))
767 return (methodName, method)
768
769
770def createNextMethod(methodName):
771 """Creates any _next methods for attaching to a Resource.
772
773 The _next methods allow for easy iteration through list() responses.
774
775 Args:
776 methodName: string, name of the method to use.
777 """
778 methodName = fix_method_name(methodName)
779
780 def methodNext(self, previous_request, previous_response):
781 """Retrieves the next page of results.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400782
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400783Args:
784 previous_request: The request for the previous page. (required)
785 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400786
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400787Returns:
788 A request object that you can call 'execute()' on to request the next
789 page. Returns None if there are no more items in the collection.
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500790 """
791 # Retrieve nextPageToken from previous_response
792 # Use as pageToken in previous_request to create new request.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400793
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500794 if 'nextPageToken' not in previous_response:
795 return None
Joe Gregorio3c676f92011-07-25 10:38:14 -0400796
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500797 request = copy.copy(previous_request)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400798
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500799 pageToken = previous_response['nextPageToken']
800 parsed = list(urlparse.urlparse(request.uri))
801 q = parse_qsl(parsed[4])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400802
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500803 # Find and remove old 'pageToken' value from URI
804 newq = [(key, value) for (key, value) in q if key != 'pageToken']
805 newq.append(('pageToken', pageToken))
806 parsed[4] = urllib.urlencode(newq)
807 uri = urlparse.urlunparse(parsed)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400808
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500809 request.uri = uri
Joe Gregorio3c676f92011-07-25 10:38:14 -0400810
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500811 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400812
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500813 return request
Joe Gregorio3c676f92011-07-25 10:38:14 -0400814
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500815 return (methodName, methodNext)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400816
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400817
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500818class Resource(object):
819 """A class for interacting with a resource."""
Joe Gregorioaf276d22010-12-09 14:26:58 -0500820
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500821 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
822 resourceDesc, rootDesc, schema):
823 """Build a Resource from the API description.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400824
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500825 Args:
826 http: httplib2.Http, Object to make http requests with.
827 baseUrl: string, base URL for the API. All requests are relative to this
828 URI.
829 model: apiclient.Model, converts to and from the wire format.
830 requestBuilder: class or callable that instantiates an
831 apiclient.HttpRequest object.
832 developerKey: string, key obtained from
833 https://code.google.com/apis/console
834 resourceDesc: object, section of deserialized discovery document that
835 describes a resource. Note that the top level discovery document
836 is considered a resource.
837 rootDesc: object, the entire deserialized discovery document.
838 schema: object, mapping of schema names to schema descriptions.
839 """
840 self._dynamic_attrs = []
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400841
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500842 self._http = http
843 self._baseUrl = baseUrl
844 self._model = model
845 self._developerKey = developerKey
846 self._requestBuilder = requestBuilder
847 self._resourceDesc = resourceDesc
848 self._rootDesc = rootDesc
849 self._schema = schema
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400850
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500851 self._set_service_methods()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400852
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500853 def _set_dynamic_attr(self, attr_name, value):
854 """Sets an instance attribute and tracks it in a list of dynamic attributes.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400855
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500856 Args:
857 attr_name: string; The name of the attribute to be set
858 value: The value being set on the object and tracked in the dynamic cache.
859 """
860 self._dynamic_attrs.append(attr_name)
861 self.__dict__[attr_name] = value
Joe Gregorio48d361f2010-08-18 13:19:21 -0400862
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500863 def __getstate__(self):
864 """Trim the state down to something that can be pickled.
865
866 Uses the fact that the instance variable _dynamic_attrs holds attrs that
867 will be wiped and restored on pickle serialization.
868 """
869 state_dict = copy.copy(self.__dict__)
870 for dynamic_attr in self._dynamic_attrs:
871 del state_dict[dynamic_attr]
872 del state_dict['_dynamic_attrs']
873 return state_dict
874
875 def __setstate__(self, state):
876 """Reconstitute the state of the object from being pickled.
877
878 Uses the fact that the instance variable _dynamic_attrs holds attrs that
879 will be wiped and restored on pickle serialization.
880 """
881 self.__dict__.update(state)
882 self._dynamic_attrs = []
883 self._set_service_methods()
884
885 def _set_service_methods(self):
886 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
887 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
888 self._add_next_methods(self._resourceDesc, self._schema)
889
890 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
891 # Add basic methods to Resource
892 if 'methods' in resourceDesc:
893 for methodName, methodDesc in resourceDesc['methods'].iteritems():
894 fixedMethodName, method = createMethod(
895 methodName, methodDesc, rootDesc, schema)
896 self._set_dynamic_attr(fixedMethodName,
897 method.__get__(self, self.__class__))
898 # Add in _media methods. The functionality of the attached method will
899 # change when it sees that the method name ends in _media.
900 if methodDesc.get('supportsMediaDownload', False):
901 fixedMethodName, method = createMethod(
902 methodName + '_media', methodDesc, rootDesc, schema)
903 self._set_dynamic_attr(fixedMethodName,
904 method.__get__(self, self.__class__))
905
906 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
907 # Add in nested resources
908 if 'resources' in resourceDesc:
909
910 def createResourceMethod(methodName, methodDesc):
911 """Create a method on the Resource to access a nested Resource.
912
913 Args:
914 methodName: string, name of the method to use.
915 methodDesc: object, fragment of deserialized discovery document that
916 describes the method.
917 """
918 methodName = fix_method_name(methodName)
919
920 def methodResource(self):
921 return Resource(http=self._http, baseUrl=self._baseUrl,
922 model=self._model, developerKey=self._developerKey,
923 requestBuilder=self._requestBuilder,
924 resourceDesc=methodDesc, rootDesc=rootDesc,
925 schema=schema)
926
927 setattr(methodResource, '__doc__', 'A collection resource.')
928 setattr(methodResource, '__is_resource__', True)
929
930 return (methodName, methodResource)
931
932 for methodName, methodDesc in resourceDesc['resources'].iteritems():
933 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
934 self._set_dynamic_attr(fixedMethodName,
935 method.__get__(self, self.__class__))
936
937 def _add_next_methods(self, resourceDesc, schema):
938 # Add _next() methods
939 # Look for response bodies in schema that contain nextPageToken, and methods
940 # that take a pageToken parameter.
941 if 'methods' in resourceDesc:
942 for methodName, methodDesc in resourceDesc['methods'].iteritems():
943 if 'response' in methodDesc:
944 responseSchema = methodDesc['response']
945 if '$ref' in responseSchema:
946 responseSchema = schema.get(responseSchema['$ref'])
947 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
948 {})
949 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
950 if hasNextPageToken and hasPageToken:
951 fixedMethodName, method = createNextMethod(methodName + '_next')
952 self._set_dynamic_attr(fixedMethodName,
953 method.__get__(self, self.__class__))