blob: 4c6cb60fc8934420c5e214af4910c4a605848e0f [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
Daniel Hermesf7b648f2013-03-06 09:38:53 -080028
29# Standard library imports
Joe Gregorio3c676f92011-07-25 10:38:14 -040030import copy
Daniel Hermesf7b648f2013-03-06 09:38:53 -080031from email.mime.multipart import MIMEMultipart
32from email.mime.nonmultipart import MIMENonMultipart
Daniel Hermesc2113242013-02-27 10:16:13 -080033import keyword
ade@google.com850cf552010-08-20 23:24:56 +010034import logging
Daniel Hermesf7b648f2013-03-06 09:38:53 -080035import mimetypes
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040036import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040037import re
Joe Gregoriofe695fb2010-08-30 12:04:04 -040038import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040039import urlparse
Joe Gregorio922b78c2011-05-26 21:36:34 -040040
ade@google.comc5eb46f2010-09-27 23:35:39 +010041try:
Joe Gregoriodc106fc2012-11-20 14:30:14 -050042 from urlparse import parse_qsl
ade@google.comc5eb46f2010-09-27 23:35:39 +010043except ImportError:
Joe Gregoriodc106fc2012-11-20 14:30:14 -050044 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050045
Daniel Hermesf7b648f2013-03-06 09:38:53 -080046# Third-party imports
47import httplib2
48import mimeparse
49import uritemplate
50
51# Local imports
Joe Gregorio2b781282011-12-08 12:00:25 -050052from apiclient.errors import HttpError
53from apiclient.errors import InvalidJsonError
54from apiclient.errors import MediaUploadSizeError
55from apiclient.errors import UnacceptableMimeTypeError
56from apiclient.errors import UnknownApiNameOrVersion
Joe Gregoriodc106fc2012-11-20 14:30:14 -050057from apiclient.errors import UnknownFileType
Joe Gregorio2b781282011-12-08 12:00:25 -050058from apiclient.http import HttpRequest
59from apiclient.http import MediaFileUpload
60from apiclient.http import MediaUpload
61from apiclient.model import JsonModel
Joe Gregorio708388c2012-06-15 13:43:04 -040062from apiclient.model import MediaModel
Joe Gregorio2b781282011-12-08 12:00:25 -050063from apiclient.model import RawModel
64from apiclient.schema import Schemas
Joe Gregorio549230c2012-01-11 10:38:05 -050065from oauth2client.anyjson import simplejson
Daniel Hermesf7b648f2013-03-06 09:38:53 -080066from oauth2client.util import _add_query_parameter
67from oauth2client.util import positional
68
Joe Gregorio2b781282011-12-08 12:00:25 -050069
Joe Gregorio504a17f2012-12-07 14:14:26 -050070# The client library requires a version of httplib2 that supports RETRIES.
71httplib2.RETRIES = 1
72
Joe Gregorioe84c9442012-03-12 08:45:57 -040073logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040074
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050075URITEMPLATE = re.compile('{[^}]*}')
76VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040077DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
Daniel Hermesc2113242013-02-27 10:16:13 -080078 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050079DEFAULT_METHOD_DOC = 'A description of how to use this function'
Daniel Hermesc2113242013-02-27 10:16:13 -080080HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH'])
81_MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40}
82BODY_PARAMETER_DEFAULT_VALUE = {
83 'description': 'The request body.',
84 'type': 'object',
85 'required': True,
86}
87MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
Daniel Hermesf7b648f2013-03-06 09:38:53 -080088 'description': ('The filename of the media request body, or an instance '
89 'of a MediaUpload object.'),
90 'type': 'string',
91 'required': False,
Daniel Hermesc2113242013-02-27 10:16:13 -080092}
Joe Gregorioca876e42011-02-22 19:39:42 -050093
Joe Gregorioc8e421c2012-06-06 14:03:13 -040094# Parameters accepted by the stack, but not visible via discovery.
Daniel Hermesc2113242013-02-27 10:16:13 -080095# TODO(dhermes): Remove 'userip' in 'v2'.
96STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict'])
97STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'}
Joe Gregorio48d361f2010-08-18 13:19:21 -040098
Daniel Hermesc2113242013-02-27 10:16:13 -080099# Library-specific reserved words beyond Python keywords.
100RESERVED_WORDS = frozenset(['body'])
Joe Gregoriod92897c2011-07-07 11:44:56 -0400101
Joe Gregorio562b7312011-09-15 09:06:38 -0400102
Joe Gregorioce31a972012-06-06 15:48:17 -0400103def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400104 """Fix method names to avoid reserved word conflicts.
105
106 Args:
107 name: string, method name.
108
109 Returns:
110 The name with a '_' prefixed if the name is a reserved word.
111 """
Daniel Hermesc2113242013-02-27 10:16:13 -0800112 if keyword.iskeyword(name) or name in RESERVED_WORDS:
Joe Gregoriod92897c2011-07-07 11:44:56 -0400113 return name + '_'
114 else:
115 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -0400116
Joe Gregorioa98733f2011-09-16 10:12:28 -0400117
Joe Gregorio48d361f2010-08-18 13:19:21 -0400118def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500119 """Converts key names into parameter names.
120
121 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400122
123 Args:
124 key: string, the method key name.
125
126 Returns:
127 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400128 """
129 result = []
130 key = list(key)
131 if not key[0].isalpha():
132 result.append('x')
133 for c in key:
134 if c.isalnum():
135 result.append(c)
136 else:
137 result.append('_')
138
139 return ''.join(result)
140
141
Joe Gregoriof4839b02012-09-06 13:47:24 -0400142@positional(2)
Joe Gregorio01770a52012-02-24 11:11:10 -0500143def build(serviceName,
144 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500145 http=None,
146 discoveryServiceUrl=DISCOVERY_URI,
147 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500148 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500149 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500150 """Construct a Resource for interacting with an API.
151
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400152 Construct a Resource object for interacting with an API. The serviceName and
153 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500154
155 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400156 serviceName: string, name of the service.
157 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500158 http: httplib2.Http, An instance of httplib2.Http or something that acts
159 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400160 discoveryServiceUrl: string, a URI Template that points to the location of
161 the discovery service. It should have two parameters {api} and
162 {apiVersion} that when filled in produce an absolute URI to the discovery
163 document for that service.
164 developerKey: string, key obtained from
165 https://code.google.com/apis/console.
166 model: apiclient.Model, converts to and from the wire format.
167 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
168 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500169
170 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400171 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500172 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400173 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400174 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175 'apiVersion': version
176 }
ade@google.com850cf552010-08-20 23:24:56 +0100177
Joe Gregorioc204b642010-09-21 12:01:23 -0400178 if http is None:
179 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400180
ade@google.com850cf552010-08-20 23:24:56 +0100181 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400182
Joe Gregorio66f57522011-11-30 11:00:00 -0500183 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
184 # variable that contains the network address of the client sending the
185 # request. If it exists then add that to the request for the discovery
186 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400187 if 'REMOTE_ADDR' in os.environ:
188 requested_url = _add_query_parameter(requested_url, 'userIp',
189 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400190 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400191
ade@google.com850cf552010-08-20 23:24:56 +0100192 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400193
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500194 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500195 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500196 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400197 if resp.status >= 400:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400198 raise HttpError(resp, content, uri=requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400199
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500200 try:
201 service = simplejson.loads(content)
202 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400203 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500204 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400205
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400206 return build_from_document(content, base=discoveryServiceUrl, http=http,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400207 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500208
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500209
Joe Gregoriof4839b02012-09-06 13:47:24 -0400210@positional(1)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500211def build_from_document(
212 service,
Joe Gregorioa2838152012-07-16 11:52:17 -0400213 base=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500214 future=None,
215 http=None,
216 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500217 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500218 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500219 """Create a Resource for interacting with an API.
220
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400221 Same as `build()`, but constructs the Resource object from a discovery
222 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500223
Joe Gregorio292b9b82011-01-12 11:36:11 -0500224 Args:
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500225 service: string or object, the JSON discovery document describing the API.
226 The value passed in may either be the JSON string or the deserialized
227 JSON.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400228 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400229 This parameter is no longer used as rootUrl and servicePath are included
230 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400231 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500232 http: httplib2.Http, An instance of httplib2.Http or something that acts
233 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500234 developerKey: string, Key for controlling API usage, generated
235 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400236 model: Model class instance that serializes and de-serializes requests and
237 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500238 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500239
240 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400241 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500242 """
243
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400244 # future is no longer used.
245 future = {}
246
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500247 if isinstance(service, basestring):
248 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400249 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500250 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400251
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500252 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500253 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500254 model = JsonModel('dataWrapper' in features)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500255 return Resource(http=http, baseUrl=base, model=model,
256 developerKey=developerKey, requestBuilder=requestBuilder,
257 resourceDesc=service, rootDesc=service, schema=schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400258
259
Joe Gregorio61d7e962011-02-22 22:52:07 -0500260def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500261 """Convert value to a string based on JSON Schema type.
262
263 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
264 JSON Schema.
265
266 Args:
267 value: any, the value to convert
268 schema_type: string, the type that value should be interpreted as
269
270 Returns:
271 A string representation of 'value' based on the schema_type.
272 """
273 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500274 if type(value) == type('') or type(value) == type(u''):
275 return value
276 else:
277 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500278 elif schema_type == 'integer':
279 return str(int(value))
280 elif schema_type == 'number':
281 return str(float(value))
282 elif schema_type == 'boolean':
283 return str(bool(value)).lower()
284 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500285 if type(value) == type('') or type(value) == type(u''):
286 return value
287 else:
288 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500289
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400290
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400291def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400292 """Convert a string media size, such as 10GB or 3TB into an integer.
293
294 Args:
295 maxSize: string, size as a string, such as 2MB or 7GB.
296
297 Returns:
298 The size as an integer value.
299 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400300 if len(maxSize) < 2:
Daniel Hermesc2113242013-02-27 10:16:13 -0800301 return 0L
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400302 units = maxSize[-2:].upper()
Daniel Hermesc2113242013-02-27 10:16:13 -0800303 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
304 if bit_shift is not None:
305 return long(maxSize[:-2]) << bit_shift
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400306 else:
Daniel Hermesc2113242013-02-27 10:16:13 -0800307 return long(maxSize)
308
309
310def _media_path_url_from_info(root_desc, path_url):
311 """Creates an absolute media path URL.
312
313 Constructed using the API root URI and service path from the discovery
314 document and the relative path for the API method.
315
316 Args:
317 root_desc: Dictionary; the entire original deserialized discovery document.
318 path_url: String; the relative URL for the API method. Relative to the API
319 root, which is specified in the discovery document.
320
321 Returns:
322 String; the absolute URI for media upload for the API method.
323 """
324 return '%(root)supload/%(service_path)s%(path)s' % {
325 'root': root_desc['rootUrl'],
326 'service_path': root_desc['servicePath'],
327 'path': path_url,
328 }
329
330
331def _fix_up_parameters(method_desc, root_desc, http_method):
332 """Updates parameters of an API method with values specific to this library.
333
334 Specifically, adds whatever global parameters are specified by the API to the
335 parameters for the individual method. Also adds parameters which don't
336 appear in the discovery document, but are available to all discovery based
337 APIs (these are listed in STACK_QUERY_PARAMETERS).
338
339 SIDE EFFECTS: This updates the parameters dictionary object in the method
340 description.
341
342 Args:
343 method_desc: Dictionary with metadata describing an API method. Value comes
344 from the dictionary of methods stored in the 'methods' key in the
345 deserialized discovery document.
346 root_desc: Dictionary; the entire original deserialized discovery document.
347 http_method: String; the HTTP method used to call the API method described
348 in method_desc.
349
350 Returns:
351 The updated Dictionary stored in the 'parameters' key of the method
352 description dictionary.
353 """
354 parameters = method_desc.setdefault('parameters', {})
355
356 # Add in the parameters common to all methods.
357 for name, description in root_desc.get('parameters', {}).iteritems():
358 parameters[name] = description
359
360 # Add in undocumented query parameters.
361 for name in STACK_QUERY_PARAMETERS:
362 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
363
364 # Add 'body' (our own reserved word) to parameters if the method supports
365 # a request payload.
366 if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc:
367 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
368 body.update(method_desc['request'])
369 parameters['body'] = body
370
371 return parameters
372
373
374def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
375 """Updates parameters of API by adding 'media_body' if supported by method.
376
377 SIDE EFFECTS: If the method supports media upload and has a required body,
378 sets body to be optional (required=False) instead. Also, if there is a
379 'mediaUpload' in the method description, adds 'media_upload' key to
380 parameters.
381
382 Args:
383 method_desc: Dictionary with metadata describing an API method. Value comes
384 from the dictionary of methods stored in the 'methods' key in the
385 deserialized discovery document.
386 root_desc: Dictionary; the entire original deserialized discovery document.
387 path_url: String; the relative URL for the API method. Relative to the API
388 root, which is specified in the discovery document.
389 parameters: A dictionary describing method parameters for method described
390 in method_desc.
391
392 Returns:
393 Triple (accept, max_size, media_path_url) where:
394 - accept is a list of strings representing what content types are
395 accepted for media upload. Defaults to empty list if not in the
396 discovery document.
397 - max_size is a long representing the max size in bytes allowed for a
398 media upload. Defaults to 0L if not in the discovery document.
399 - media_path_url is a String; the absolute URI for media upload for the
400 API method. Constructed using the API root URI and service path from
401 the discovery document and the relative path for the API method. If
402 media upload is not supported, this is None.
403 """
404 media_upload = method_desc.get('mediaUpload', {})
405 accept = media_upload.get('accept', [])
406 max_size = _media_size_to_long(media_upload.get('maxSize', ''))
407 media_path_url = None
408
409 if media_upload:
410 media_path_url = _media_path_url_from_info(root_desc, path_url)
411 parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
412 if 'body' in parameters:
413 parameters['body']['required'] = False
414
415 return accept, max_size, media_path_url
416
417
418def _fix_up_method_description(method_desc, root_desc):
419 """Updates a method description in a discovery document.
420
421 SIDE EFFECTS: Changes the parameters dictionary in the method description with
422 extra parameters which are used locally.
423
424 Args:
425 method_desc: Dictionary with metadata describing an API method. Value comes
426 from the dictionary of methods stored in the 'methods' key in the
427 deserialized discovery document.
428 root_desc: Dictionary; the entire original deserialized discovery document.
429
430 Returns:
431 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
432 where:
433 - path_url is a String; the relative URL for the API method. Relative to
434 the API root, which is specified in the discovery document.
435 - http_method is a String; the HTTP method used to call the API method
436 described in the method description.
437 - method_id is a String; the name of the RPC method associated with the
438 API method, and is in the method description in the 'id' key.
439 - accept is a list of strings representing what content types are
440 accepted for media upload. Defaults to empty list if not in the
441 discovery document.
442 - max_size is a long representing the max size in bytes allowed for a
443 media upload. Defaults to 0L if not in the discovery document.
444 - media_path_url is a String; the absolute URI for media upload for the
445 API method. Constructed using the API root URI and service path from
446 the discovery document and the relative path for the API method. If
447 media upload is not supported, this is None.
448 """
449 path_url = method_desc['path']
450 http_method = method_desc['httpMethod']
451 method_id = method_desc['id']
452
453 parameters = _fix_up_parameters(method_desc, root_desc, http_method)
454 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
455 # 'parameters' key and needs to know if there is a 'body' parameter because it
456 # also sets a 'media_body' parameter.
457 accept, max_size, media_path_url = _fix_up_media_upload(
458 method_desc, root_desc, path_url, parameters)
459
460 return path_url, http_method, method_id, accept, max_size, media_path_url
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400461
Joe Gregoriobee86832011-02-22 10:00:19 -0500462
Daniel Hermes954e1242013-02-28 09:28:37 -0800463# TODO(dhermes): Convert this class to ResourceMethod and make it callable
464class ResourceMethodParameters(object):
465 """Represents the parameters associated with a method.
466
467 Attributes:
468 argmap: Map from method parameter name (string) to query parameter name
469 (string).
470 required_params: List of required parameters (represented by parameter
471 name as string).
472 repeated_params: List of repeated parameters (represented by parameter
473 name as string).
474 pattern_params: Map from method parameter name (string) to regular
475 expression (as a string). If the pattern is set for a parameter, the
476 value for that parameter must match the regular expression.
477 query_params: List of parameters (represented by parameter name as string)
478 that will be used in the query string.
479 path_params: Set of parameters (represented by parameter name as string)
480 that will be used in the base URL path.
481 param_types: Map from method parameter name (string) to parameter type. Type
482 can be any valid JSON schema type; valid values are 'any', 'array',
483 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
484 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
485 enum_params: Map from method parameter name (string) to list of strings,
486 where each list of strings is the list of acceptable enum values.
487 """
488
489 def __init__(self, method_desc):
490 """Constructor for ResourceMethodParameters.
491
492 Sets default values and defers to set_parameters to populate.
493
494 Args:
495 method_desc: Dictionary with metadata describing an API method. Value
496 comes from the dictionary of methods stored in the 'methods' key in
497 the deserialized discovery document.
498 """
499 self.argmap = {}
500 self.required_params = []
501 self.repeated_params = []
502 self.pattern_params = {}
503 self.query_params = []
504 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
505 # parsing is gotten rid of.
506 self.path_params = set()
507 self.param_types = {}
508 self.enum_params = {}
509
510 self.set_parameters(method_desc)
511
512 def set_parameters(self, method_desc):
513 """Populates maps and lists based on method description.
514
515 Iterates through each parameter for the method and parses the values from
516 the parameter dictionary.
517
518 Args:
519 method_desc: Dictionary with metadata describing an API method. Value
520 comes from the dictionary of methods stored in the 'methods' key in
521 the deserialized discovery document.
522 """
523 for arg, desc in method_desc.get('parameters', {}).iteritems():
524 param = key2param(arg)
525 self.argmap[param] = arg
526
527 if desc.get('pattern'):
528 self.pattern_params[param] = desc['pattern']
529 if desc.get('enum'):
530 self.enum_params[param] = desc['enum']
531 if desc.get('required'):
532 self.required_params.append(param)
533 if desc.get('repeated'):
534 self.repeated_params.append(param)
535 if desc.get('location') == 'query':
536 self.query_params.append(param)
537 if desc.get('location') == 'path':
538 self.path_params.add(param)
539 self.param_types[param] = desc.get('type', 'string')
540
541 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
542 # should have all path parameters already marked with
543 # 'location: path'.
544 for match in URITEMPLATE.finditer(method_desc['path']):
545 for namematch in VARNAME.finditer(match.group(0)):
546 name = key2param(namematch.group(0))
547 self.path_params.add(name)
548 if name in self.query_params:
549 self.query_params.remove(name)
550
551
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500552def createMethod(methodName, methodDesc, rootDesc, schema):
553 """Creates a method for attaching to a Resource.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400554
555 Args:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500556 methodName: string, name of the method to use.
557 methodDesc: object, fragment of deserialized discovery document that
558 describes the method.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400559 rootDesc: object, the entire deserialized discovery document.
560 schema: object, mapping of schema names to schema descriptions.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400561 """
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500562 methodName = fix_method_name(methodName)
Daniel Hermesc2113242013-02-27 10:16:13 -0800563 (pathUrl, httpMethod, methodId, accept,
564 maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400565
Daniel Hermes954e1242013-02-28 09:28:37 -0800566 parameters = ResourceMethodParameters(methodDesc)
ade@google.com850cf552010-08-20 23:24:56 +0100567
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500568 def method(self, **kwargs):
569 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorioca876e42011-02-22 19:39:42 -0500570
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500571 for name in kwargs.iterkeys():
Daniel Hermes954e1242013-02-28 09:28:37 -0800572 if name not in parameters.argmap:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500573 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorioca876e42011-02-22 19:39:42 -0500574
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500575 # Remove args that have a value of None.
576 keys = kwargs.keys()
577 for name in keys:
578 if kwargs[name] is None:
579 del kwargs[name]
Joe Gregorio21f11672010-08-18 17:23:17 -0400580
Daniel Hermes954e1242013-02-28 09:28:37 -0800581 for name in parameters.required_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500582 if name not in kwargs:
583 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400584
Daniel Hermes954e1242013-02-28 09:28:37 -0800585 for name, regex in parameters.pattern_params.iteritems():
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500586 if name in kwargs:
587 if isinstance(kwargs[name], basestring):
588 pvalues = [kwargs[name]]
Joe Gregorio61d7e962011-02-22 22:52:07 -0500589 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500590 pvalues = kwargs[name]
591 for pvalue in pvalues:
592 if re.match(regex, pvalue) is None:
593 raise TypeError(
594 'Parameter "%s" value "%s" does not match the pattern "%s"' %
595 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400596
Daniel Hermes954e1242013-02-28 09:28:37 -0800597 for name, enums in parameters.enum_params.iteritems():
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500598 if name in kwargs:
599 # We need to handle the case of a repeated enum
600 # name differently, since we want to handle both
601 # arg='value' and arg=['value1', 'value2']
Daniel Hermes954e1242013-02-28 09:28:37 -0800602 if (name in parameters.repeated_params and
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500603 not isinstance(kwargs[name], basestring)):
604 values = kwargs[name]
605 else:
606 values = [kwargs[name]]
607 for value in values:
608 if value not in enums:
609 raise TypeError(
610 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
611 (name, value, str(enums)))
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400612
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500613 actual_query_params = {}
614 actual_path_params = {}
615 for key, value in kwargs.iteritems():
Daniel Hermes954e1242013-02-28 09:28:37 -0800616 to_type = parameters.param_types.get(key, 'string')
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500617 # For repeated parameters we cast each member of the list.
Daniel Hermes954e1242013-02-28 09:28:37 -0800618 if key in parameters.repeated_params and type(value) == type([]):
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500619 cast_value = [_cast(x, to_type) for x in value]
620 else:
621 cast_value = _cast(value, to_type)
Daniel Hermes954e1242013-02-28 09:28:37 -0800622 if key in parameters.query_params:
623 actual_query_params[parameters.argmap[key]] = cast_value
624 if key in parameters.path_params:
625 actual_path_params[parameters.argmap[key]] = cast_value
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500626 body_value = kwargs.get('body', None)
627 media_filename = kwargs.get('media_body', None)
Joe Gregorioe08a1662011-12-07 09:48:22 -0500628
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500629 if self._developerKey:
630 actual_query_params['key'] = self._developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400631
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500632 model = self._model
633 if methodName.endswith('_media'):
634 model = MediaModel()
635 elif 'response' not in methodDesc:
636 model = RawModel()
637
638 headers = {}
639 headers, params, query, body = model.request(headers,
640 actual_path_params, actual_query_params, body_value)
641
642 expanded_url = uritemplate.expand(pathUrl, params)
643 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
644
645 resumable = None
646 multipart_boundary = ''
647
648 if media_filename:
649 # Ensure we end up with a valid MediaUpload object.
650 if isinstance(media_filename, basestring):
651 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
652 if media_mime_type is None:
653 raise UnknownFileType(media_filename)
654 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
655 raise UnacceptableMimeTypeError(media_mime_type)
656 media_upload = MediaFileUpload(media_filename,
657 mimetype=media_mime_type)
658 elif isinstance(media_filename, MediaUpload):
659 media_upload = media_filename
660 else:
661 raise TypeError('media_filename must be str or MediaUpload.')
662
663 # Check the maxSize
664 if maxSize > 0 and media_upload.size() > maxSize:
665 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
666
667 # Use the media path uri for media uploads
668 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400669 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500670 if media_upload.resumable():
671 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400672
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500673 if media_upload.resumable():
674 # This is all we need to do for resumable, if the body exists it gets
675 # sent in the first request, otherwise an empty body is sent.
676 resumable = media_upload
Joe Gregorio2b781282011-12-08 12:00:25 -0500677 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500678 # A non-resumable upload
679 if body is None:
680 # This is a simple media upload
681 headers['content-type'] = media_upload.mimetype()
682 body = media_upload.getbytes(0, media_upload.size())
683 url = _add_query_parameter(url, 'uploadType', 'media')
684 else:
685 # This is a multipart/related upload.
686 msgRoot = MIMEMultipart('related')
687 # msgRoot should not write out it's own headers
688 setattr(msgRoot, '_write_headers', lambda self: None)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400689
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500690 # attach the body as one part
691 msg = MIMENonMultipart(*headers['content-type'].split('/'))
692 msg.set_payload(body)
693 msgRoot.attach(msg)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400694
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500695 # attach the media as the second part
696 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
697 msg['Content-Transfer-Encoding'] = 'binary'
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400698
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500699 payload = media_upload.getbytes(0, media_upload.size())
700 msg.set_payload(payload)
701 msgRoot.attach(msg)
702 body = msgRoot.as_string()
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400703
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500704 multipart_boundary = msgRoot.get_boundary()
705 headers['content-type'] = ('multipart/related; '
706 'boundary="%s"') % multipart_boundary
707 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400708
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500709 logger.info('URL being requested: %s' % url)
710 return self._requestBuilder(self._http,
711 model.response,
712 url,
713 method=httpMethod,
714 body=body,
715 headers=headers,
716 methodId=methodId,
717 resumable=resumable)
718
719 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
Daniel Hermes954e1242013-02-28 09:28:37 -0800720 if len(parameters.argmap) > 0:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500721 docs.append('Args:\n')
722
723 # Skip undocumented params and params common to all methods.
724 skip_parameters = rootDesc.get('parameters', {}).keys()
725 skip_parameters.extend(STACK_QUERY_PARAMETERS)
726
Daniel Hermes954e1242013-02-28 09:28:37 -0800727 all_args = parameters.argmap.keys()
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500728 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
729
730 # Move body to the front of the line.
731 if 'body' in all_args:
732 args_ordered.append('body')
733
734 for name in all_args:
735 if name not in args_ordered:
736 args_ordered.append(name)
737
738 for arg in args_ordered:
739 if arg in skip_parameters:
740 continue
741
742 repeated = ''
Daniel Hermes954e1242013-02-28 09:28:37 -0800743 if arg in parameters.repeated_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500744 repeated = ' (repeated)'
745 required = ''
Daniel Hermes954e1242013-02-28 09:28:37 -0800746 if arg in parameters.required_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500747 required = ' (required)'
Daniel Hermes954e1242013-02-28 09:28:37 -0800748 paramdesc = methodDesc['parameters'][parameters.argmap[arg]]
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500749 paramdoc = paramdesc.get('description', 'A parameter')
750 if '$ref' in paramdesc:
751 docs.append(
752 (' %s: object, %s%s%s\n The object takes the'
753 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
754 schema.prettyPrintByName(paramdesc['$ref'])))
755 else:
756 paramtype = paramdesc.get('type', 'string')
757 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
758 repeated))
759 enum = paramdesc.get('enum', [])
760 enumDesc = paramdesc.get('enumDescriptions', [])
761 if enum and enumDesc:
762 docs.append(' Allowed values\n')
763 for (name, desc) in zip(enum, enumDesc):
764 docs.append(' %s - %s\n' % (name, desc))
765 if 'response' in methodDesc:
766 if methodName.endswith('_media'):
767 docs.append('\nReturns:\n The media object as a string.\n\n ')
768 else:
769 docs.append('\nReturns:\n An object of the form:\n\n ')
770 docs.append(schema.prettyPrintSchema(methodDesc['response']))
771
772 setattr(method, '__doc__', ''.join(docs))
773 return (methodName, method)
774
775
776def createNextMethod(methodName):
777 """Creates any _next methods for attaching to a Resource.
778
779 The _next methods allow for easy iteration through list() responses.
780
781 Args:
782 methodName: string, name of the method to use.
783 """
784 methodName = fix_method_name(methodName)
785
786 def methodNext(self, previous_request, previous_response):
787 """Retrieves the next page of results.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400788
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400789Args:
790 previous_request: The request for the previous page. (required)
791 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400792
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400793Returns:
794 A request object that you can call 'execute()' on to request the next
795 page. Returns None if there are no more items in the collection.
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500796 """
797 # Retrieve nextPageToken from previous_response
798 # Use as pageToken in previous_request to create new request.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400799
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500800 if 'nextPageToken' not in previous_response:
801 return None
Joe Gregorio3c676f92011-07-25 10:38:14 -0400802
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500803 request = copy.copy(previous_request)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400804
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500805 pageToken = previous_response['nextPageToken']
806 parsed = list(urlparse.urlparse(request.uri))
807 q = parse_qsl(parsed[4])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400808
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500809 # Find and remove old 'pageToken' value from URI
810 newq = [(key, value) for (key, value) in q if key != 'pageToken']
811 newq.append(('pageToken', pageToken))
812 parsed[4] = urllib.urlencode(newq)
813 uri = urlparse.urlunparse(parsed)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400814
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500815 request.uri = uri
Joe Gregorio3c676f92011-07-25 10:38:14 -0400816
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500817 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400818
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500819 return request
Joe Gregorio3c676f92011-07-25 10:38:14 -0400820
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500821 return (methodName, methodNext)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400822
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400823
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500824class Resource(object):
825 """A class for interacting with a resource."""
Joe Gregorioaf276d22010-12-09 14:26:58 -0500826
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500827 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
828 resourceDesc, rootDesc, schema):
829 """Build a Resource from the API description.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400830
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500831 Args:
832 http: httplib2.Http, Object to make http requests with.
833 baseUrl: string, base URL for the API. All requests are relative to this
834 URI.
835 model: apiclient.Model, converts to and from the wire format.
836 requestBuilder: class or callable that instantiates an
837 apiclient.HttpRequest object.
838 developerKey: string, key obtained from
839 https://code.google.com/apis/console
840 resourceDesc: object, section of deserialized discovery document that
841 describes a resource. Note that the top level discovery document
842 is considered a resource.
843 rootDesc: object, the entire deserialized discovery document.
844 schema: object, mapping of schema names to schema descriptions.
845 """
846 self._dynamic_attrs = []
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400847
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500848 self._http = http
849 self._baseUrl = baseUrl
850 self._model = model
851 self._developerKey = developerKey
852 self._requestBuilder = requestBuilder
853 self._resourceDesc = resourceDesc
854 self._rootDesc = rootDesc
855 self._schema = schema
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400856
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500857 self._set_service_methods()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400858
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500859 def _set_dynamic_attr(self, attr_name, value):
860 """Sets an instance attribute and tracks it in a list of dynamic attributes.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400861
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500862 Args:
863 attr_name: string; The name of the attribute to be set
864 value: The value being set on the object and tracked in the dynamic cache.
865 """
866 self._dynamic_attrs.append(attr_name)
867 self.__dict__[attr_name] = value
Joe Gregorio48d361f2010-08-18 13:19:21 -0400868
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500869 def __getstate__(self):
870 """Trim the state down to something that can be pickled.
871
872 Uses the fact that the instance variable _dynamic_attrs holds attrs that
873 will be wiped and restored on pickle serialization.
874 """
875 state_dict = copy.copy(self.__dict__)
876 for dynamic_attr in self._dynamic_attrs:
877 del state_dict[dynamic_attr]
878 del state_dict['_dynamic_attrs']
879 return state_dict
880
881 def __setstate__(self, state):
882 """Reconstitute the state of the object from being pickled.
883
884 Uses the fact that the instance variable _dynamic_attrs holds attrs that
885 will be wiped and restored on pickle serialization.
886 """
887 self.__dict__.update(state)
888 self._dynamic_attrs = []
889 self._set_service_methods()
890
891 def _set_service_methods(self):
892 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
893 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
894 self._add_next_methods(self._resourceDesc, self._schema)
895
896 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
897 # Add basic methods to Resource
898 if 'methods' in resourceDesc:
899 for methodName, methodDesc in resourceDesc['methods'].iteritems():
900 fixedMethodName, method = createMethod(
901 methodName, methodDesc, rootDesc, schema)
902 self._set_dynamic_attr(fixedMethodName,
903 method.__get__(self, self.__class__))
904 # Add in _media methods. The functionality of the attached method will
905 # change when it sees that the method name ends in _media.
906 if methodDesc.get('supportsMediaDownload', False):
907 fixedMethodName, method = createMethod(
908 methodName + '_media', methodDesc, rootDesc, schema)
909 self._set_dynamic_attr(fixedMethodName,
910 method.__get__(self, self.__class__))
911
912 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
913 # Add in nested resources
914 if 'resources' in resourceDesc:
915
916 def createResourceMethod(methodName, methodDesc):
917 """Create a method on the Resource to access a nested Resource.
918
919 Args:
920 methodName: string, name of the method to use.
921 methodDesc: object, fragment of deserialized discovery document that
922 describes the method.
923 """
924 methodName = fix_method_name(methodName)
925
926 def methodResource(self):
927 return Resource(http=self._http, baseUrl=self._baseUrl,
928 model=self._model, developerKey=self._developerKey,
929 requestBuilder=self._requestBuilder,
930 resourceDesc=methodDesc, rootDesc=rootDesc,
931 schema=schema)
932
933 setattr(methodResource, '__doc__', 'A collection resource.')
934 setattr(methodResource, '__is_resource__', True)
935
936 return (methodName, methodResource)
937
938 for methodName, methodDesc in resourceDesc['resources'].iteritems():
939 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
940 self._set_dynamic_attr(fixedMethodName,
941 method.__get__(self, self.__class__))
942
943 def _add_next_methods(self, resourceDesc, schema):
944 # Add _next() methods
945 # Look for response bodies in schema that contain nextPageToken, and methods
946 # that take a pageToken parameter.
947 if 'methods' in resourceDesc:
948 for methodName, methodDesc in resourceDesc['methods'].iteritems():
949 if 'response' in methodDesc:
950 responseSchema = methodDesc['response']
951 if '$ref' in responseSchema:
952 responseSchema = schema.get(responseSchema['$ref'])
953 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
954 {})
955 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
956 if hasNextPageToken and hasPageToken:
957 fixedMethodName, method = createNextMethod(methodName + '_next')
958 self._set_dynamic_attr(fixedMethodName,
959 method.__get__(self, self.__class__))