blob: 580db0e9774e9e13a3aef8ac138a14e1dcfb0934 [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 Gregorio549230c2012-01-11 10:38:05 -050061from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050062
Joe Gregorio504a17f2012-12-07 14:14:26 -050063# The client library requires a version of httplib2 that supports RETRIES.
64httplib2.RETRIES = 1
65
Joe Gregorioe84c9442012-03-12 08:45:57 -040066logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040067
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050068URITEMPLATE = re.compile('{[^}]*}')
69VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040070DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
Daniel Hermesc2113242013-02-27 10:16:13 -080071 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050072DEFAULT_METHOD_DOC = 'A description of how to use this function'
Daniel Hermesc2113242013-02-27 10:16:13 -080073HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH'])
74_MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40}
75BODY_PARAMETER_DEFAULT_VALUE = {
76 'description': 'The request body.',
77 'type': 'object',
78 'required': True,
79}
80MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
81 'description': ('The filename of the media request body, or an instance '
82 'of a MediaUpload object.'),
83 'type': 'string',
84 'required': False,
85}
Joe Gregorioca876e42011-02-22 19:39:42 -050086
Joe Gregorioc8e421c2012-06-06 14:03:13 -040087# Parameters accepted by the stack, but not visible via discovery.
Daniel Hermesc2113242013-02-27 10:16:13 -080088# TODO(dhermes): Remove 'userip' in 'v2'.
89STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict'])
90STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'}
Joe Gregorio48d361f2010-08-18 13:19:21 -040091
Daniel Hermesc2113242013-02-27 10:16:13 -080092# Library-specific reserved words beyond Python keywords.
93RESERVED_WORDS = frozenset(['body'])
Joe Gregoriod92897c2011-07-07 11:44:56 -040094
Joe Gregorio562b7312011-09-15 09:06:38 -040095
Joe Gregorioce31a972012-06-06 15:48:17 -040096def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040097 """Fix method names to avoid reserved word conflicts.
98
99 Args:
100 name: string, method name.
101
102 Returns:
103 The name with a '_' prefixed if the name is a reserved word.
104 """
Daniel Hermesc2113242013-02-27 10:16:13 -0800105 if keyword.iskeyword(name) or name in RESERVED_WORDS:
Joe Gregoriod92897c2011-07-07 11:44:56 -0400106 return name + '_'
107 else:
108 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -0400109
Joe Gregorioa98733f2011-09-16 10:12:28 -0400110
Joe Gregorioa98733f2011-09-16 10:12:28 -0400111def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -0500112 """Adds a query parameter to a url.
113
114 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400115
116 Args:
117 url: string, url to add the query parameter to.
118 name: string, query parameter name.
119 value: string, query parameter value.
120
121 Returns:
122 Updated query parameter. Does not update the url if value is None.
123 """
124 if value is None:
125 return url
126 else:
127 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500128 q = dict(parse_qsl(parsed[4]))
129 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400130 parsed[4] = urllib.urlencode(q)
131 return urlparse.urlunparse(parsed)
132
133
Joe Gregorio48d361f2010-08-18 13:19:21 -0400134def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500135 """Converts key names into parameter names.
136
137 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400138
139 Args:
140 key: string, the method key name.
141
142 Returns:
143 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400144 """
145 result = []
146 key = list(key)
147 if not key[0].isalpha():
148 result.append('x')
149 for c in key:
150 if c.isalnum():
151 result.append(c)
152 else:
153 result.append('_')
154
155 return ''.join(result)
156
157
Joe Gregoriof4839b02012-09-06 13:47:24 -0400158@positional(2)
Joe Gregorio01770a52012-02-24 11:11:10 -0500159def build(serviceName,
160 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500161 http=None,
162 discoveryServiceUrl=DISCOVERY_URI,
163 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500164 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500165 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500166 """Construct a Resource for interacting with an API.
167
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400168 Construct a Resource object for interacting with an API. The serviceName and
169 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500170
171 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400172 serviceName: string, name of the service.
173 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500174 http: httplib2.Http, An instance of httplib2.Http or something that acts
175 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400176 discoveryServiceUrl: string, a URI Template that points to the location of
177 the discovery service. It should have two parameters {api} and
178 {apiVersion} that when filled in produce an absolute URI to the discovery
179 document for that service.
180 developerKey: string, key obtained from
181 https://code.google.com/apis/console.
182 model: apiclient.Model, converts to and from the wire format.
183 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
184 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500185
186 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400187 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500188 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400189 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400190 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400191 'apiVersion': version
192 }
ade@google.com850cf552010-08-20 23:24:56 +0100193
Joe Gregorioc204b642010-09-21 12:01:23 -0400194 if http is None:
195 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400196
ade@google.com850cf552010-08-20 23:24:56 +0100197 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400198
Joe Gregorio66f57522011-11-30 11:00:00 -0500199 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
200 # variable that contains the network address of the client sending the
201 # request. If it exists then add that to the request for the discovery
202 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400203 if 'REMOTE_ADDR' in os.environ:
204 requested_url = _add_query_parameter(requested_url, 'userIp',
205 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400206 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400207
ade@google.com850cf552010-08-20 23:24:56 +0100208 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400209
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500210 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500211 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500212 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400213 if resp.status >= 400:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400214 raise HttpError(resp, content, uri=requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400215
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500216 try:
217 service = simplejson.loads(content)
218 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400219 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500220 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400221
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400222 return build_from_document(content, base=discoveryServiceUrl, http=http,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400223 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500224
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500225
Joe Gregoriof4839b02012-09-06 13:47:24 -0400226@positional(1)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500227def build_from_document(
228 service,
Joe Gregorioa2838152012-07-16 11:52:17 -0400229 base=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500230 future=None,
231 http=None,
232 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500233 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500234 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500235 """Create a Resource for interacting with an API.
236
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400237 Same as `build()`, but constructs the Resource object from a discovery
238 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500239
Joe Gregorio292b9b82011-01-12 11:36:11 -0500240 Args:
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500241 service: string or object, the JSON discovery document describing the API.
242 The value passed in may either be the JSON string or the deserialized
243 JSON.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400244 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400245 This parameter is no longer used as rootUrl and servicePath are included
246 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400247 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500248 http: httplib2.Http, An instance of httplib2.Http or something that acts
249 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500250 developerKey: string, Key for controlling API usage, generated
251 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400252 model: Model class instance that serializes and de-serializes requests and
253 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500254 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500255
256 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400257 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500258 """
259
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400260 # future is no longer used.
261 future = {}
262
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500263 if isinstance(service, basestring):
264 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400265 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500266 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400267
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500268 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500269 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500270 model = JsonModel('dataWrapper' in features)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500271 return Resource(http=http, baseUrl=base, model=model,
272 developerKey=developerKey, requestBuilder=requestBuilder,
273 resourceDesc=service, rootDesc=service, schema=schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400274
275
Joe Gregorio61d7e962011-02-22 22:52:07 -0500276def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500277 """Convert value to a string based on JSON Schema type.
278
279 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
280 JSON Schema.
281
282 Args:
283 value: any, the value to convert
284 schema_type: string, the type that value should be interpreted as
285
286 Returns:
287 A string representation of 'value' based on the schema_type.
288 """
289 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500290 if type(value) == type('') or type(value) == type(u''):
291 return value
292 else:
293 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500294 elif schema_type == 'integer':
295 return str(int(value))
296 elif schema_type == 'number':
297 return str(float(value))
298 elif schema_type == 'boolean':
299 return str(bool(value)).lower()
300 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500301 if type(value) == type('') or type(value) == type(u''):
302 return value
303 else:
304 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500305
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400306
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400307def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400308 """Convert a string media size, such as 10GB or 3TB into an integer.
309
310 Args:
311 maxSize: string, size as a string, such as 2MB or 7GB.
312
313 Returns:
314 The size as an integer value.
315 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400316 if len(maxSize) < 2:
Daniel Hermesc2113242013-02-27 10:16:13 -0800317 return 0L
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400318 units = maxSize[-2:].upper()
Daniel Hermesc2113242013-02-27 10:16:13 -0800319 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
320 if bit_shift is not None:
321 return long(maxSize[:-2]) << bit_shift
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400322 else:
Daniel Hermesc2113242013-02-27 10:16:13 -0800323 return long(maxSize)
324
325
326def _media_path_url_from_info(root_desc, path_url):
327 """Creates an absolute media path URL.
328
329 Constructed using the API root URI and service path from the discovery
330 document and the relative path for the API method.
331
332 Args:
333 root_desc: Dictionary; the entire original deserialized discovery document.
334 path_url: String; the relative URL for the API method. Relative to the API
335 root, which is specified in the discovery document.
336
337 Returns:
338 String; the absolute URI for media upload for the API method.
339 """
340 return '%(root)supload/%(service_path)s%(path)s' % {
341 'root': root_desc['rootUrl'],
342 'service_path': root_desc['servicePath'],
343 'path': path_url,
344 }
345
346
347def _fix_up_parameters(method_desc, root_desc, http_method):
348 """Updates parameters of an API method with values specific to this library.
349
350 Specifically, adds whatever global parameters are specified by the API to the
351 parameters for the individual method. Also adds parameters which don't
352 appear in the discovery document, but are available to all discovery based
353 APIs (these are listed in STACK_QUERY_PARAMETERS).
354
355 SIDE EFFECTS: This updates the parameters dictionary object in the method
356 description.
357
358 Args:
359 method_desc: Dictionary with metadata describing an API method. Value comes
360 from the dictionary of methods stored in the 'methods' key in the
361 deserialized discovery document.
362 root_desc: Dictionary; the entire original deserialized discovery document.
363 http_method: String; the HTTP method used to call the API method described
364 in method_desc.
365
366 Returns:
367 The updated Dictionary stored in the 'parameters' key of the method
368 description dictionary.
369 """
370 parameters = method_desc.setdefault('parameters', {})
371
372 # Add in the parameters common to all methods.
373 for name, description in root_desc.get('parameters', {}).iteritems():
374 parameters[name] = description
375
376 # Add in undocumented query parameters.
377 for name in STACK_QUERY_PARAMETERS:
378 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
379
380 # Add 'body' (our own reserved word) to parameters if the method supports
381 # a request payload.
382 if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc:
383 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
384 body.update(method_desc['request'])
385 parameters['body'] = body
386
387 return parameters
388
389
390def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
391 """Updates parameters of API by adding 'media_body' if supported by method.
392
393 SIDE EFFECTS: If the method supports media upload and has a required body,
394 sets body to be optional (required=False) instead. Also, if there is a
395 'mediaUpload' in the method description, adds 'media_upload' key to
396 parameters.
397
398 Args:
399 method_desc: Dictionary with metadata describing an API method. Value comes
400 from the dictionary of methods stored in the 'methods' key in the
401 deserialized discovery document.
402 root_desc: Dictionary; the entire original deserialized discovery document.
403 path_url: String; the relative URL for the API method. Relative to the API
404 root, which is specified in the discovery document.
405 parameters: A dictionary describing method parameters for method described
406 in method_desc.
407
408 Returns:
409 Triple (accept, max_size, media_path_url) where:
410 - accept is a list of strings representing what content types are
411 accepted for media upload. Defaults to empty list if not in the
412 discovery document.
413 - max_size is a long representing the max size in bytes allowed for a
414 media upload. Defaults to 0L if not in the discovery document.
415 - media_path_url is a String; the absolute URI for media upload for the
416 API method. Constructed using the API root URI and service path from
417 the discovery document and the relative path for the API method. If
418 media upload is not supported, this is None.
419 """
420 media_upload = method_desc.get('mediaUpload', {})
421 accept = media_upload.get('accept', [])
422 max_size = _media_size_to_long(media_upload.get('maxSize', ''))
423 media_path_url = None
424
425 if media_upload:
426 media_path_url = _media_path_url_from_info(root_desc, path_url)
427 parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
428 if 'body' in parameters:
429 parameters['body']['required'] = False
430
431 return accept, max_size, media_path_url
432
433
434def _fix_up_method_description(method_desc, root_desc):
435 """Updates a method description in a discovery document.
436
437 SIDE EFFECTS: Changes the parameters dictionary in the method description with
438 extra parameters which are used locally.
439
440 Args:
441 method_desc: Dictionary with metadata describing an API method. Value comes
442 from the dictionary of methods stored in the 'methods' key in the
443 deserialized discovery document.
444 root_desc: Dictionary; the entire original deserialized discovery document.
445
446 Returns:
447 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
448 where:
449 - path_url is a String; the relative URL for the API method. Relative to
450 the API root, which is specified in the discovery document.
451 - http_method is a String; the HTTP method used to call the API method
452 described in the method description.
453 - method_id is a String; the name of the RPC method associated with the
454 API method, and is in the method description in the 'id' key.
455 - accept is a list of strings representing what content types are
456 accepted for media upload. Defaults to empty list if not in the
457 discovery document.
458 - max_size is a long representing the max size in bytes allowed for a
459 media upload. Defaults to 0L if not in the discovery document.
460 - media_path_url is a String; the absolute URI for media upload for the
461 API method. Constructed using the API root URI and service path from
462 the discovery document and the relative path for the API method. If
463 media upload is not supported, this is None.
464 """
465 path_url = method_desc['path']
466 http_method = method_desc['httpMethod']
467 method_id = method_desc['id']
468
469 parameters = _fix_up_parameters(method_desc, root_desc, http_method)
470 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
471 # 'parameters' key and needs to know if there is a 'body' parameter because it
472 # also sets a 'media_body' parameter.
473 accept, max_size, media_path_url = _fix_up_media_upload(
474 method_desc, root_desc, path_url, parameters)
475
476 return path_url, http_method, method_id, accept, max_size, media_path_url
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400477
Joe Gregoriobee86832011-02-22 10:00:19 -0500478
Daniel Hermes954e1242013-02-28 09:28:37 -0800479# TODO(dhermes): Convert this class to ResourceMethod and make it callable
480class ResourceMethodParameters(object):
481 """Represents the parameters associated with a method.
482
483 Attributes:
484 argmap: Map from method parameter name (string) to query parameter name
485 (string).
486 required_params: List of required parameters (represented by parameter
487 name as string).
488 repeated_params: List of repeated parameters (represented by parameter
489 name as string).
490 pattern_params: Map from method parameter name (string) to regular
491 expression (as a string). If the pattern is set for a parameter, the
492 value for that parameter must match the regular expression.
493 query_params: List of parameters (represented by parameter name as string)
494 that will be used in the query string.
495 path_params: Set of parameters (represented by parameter name as string)
496 that will be used in the base URL path.
497 param_types: Map from method parameter name (string) to parameter type. Type
498 can be any valid JSON schema type; valid values are 'any', 'array',
499 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
500 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
501 enum_params: Map from method parameter name (string) to list of strings,
502 where each list of strings is the list of acceptable enum values.
503 """
504
505 def __init__(self, method_desc):
506 """Constructor for ResourceMethodParameters.
507
508 Sets default values and defers to set_parameters to populate.
509
510 Args:
511 method_desc: Dictionary with metadata describing an API method. Value
512 comes from the dictionary of methods stored in the 'methods' key in
513 the deserialized discovery document.
514 """
515 self.argmap = {}
516 self.required_params = []
517 self.repeated_params = []
518 self.pattern_params = {}
519 self.query_params = []
520 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
521 # parsing is gotten rid of.
522 self.path_params = set()
523 self.param_types = {}
524 self.enum_params = {}
525
526 self.set_parameters(method_desc)
527
528 def set_parameters(self, method_desc):
529 """Populates maps and lists based on method description.
530
531 Iterates through each parameter for the method and parses the values from
532 the parameter dictionary.
533
534 Args:
535 method_desc: Dictionary with metadata describing an API method. Value
536 comes from the dictionary of methods stored in the 'methods' key in
537 the deserialized discovery document.
538 """
539 for arg, desc in method_desc.get('parameters', {}).iteritems():
540 param = key2param(arg)
541 self.argmap[param] = arg
542
543 if desc.get('pattern'):
544 self.pattern_params[param] = desc['pattern']
545 if desc.get('enum'):
546 self.enum_params[param] = desc['enum']
547 if desc.get('required'):
548 self.required_params.append(param)
549 if desc.get('repeated'):
550 self.repeated_params.append(param)
551 if desc.get('location') == 'query':
552 self.query_params.append(param)
553 if desc.get('location') == 'path':
554 self.path_params.add(param)
555 self.param_types[param] = desc.get('type', 'string')
556
557 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
558 # should have all path parameters already marked with
559 # 'location: path'.
560 for match in URITEMPLATE.finditer(method_desc['path']):
561 for namematch in VARNAME.finditer(match.group(0)):
562 name = key2param(namematch.group(0))
563 self.path_params.add(name)
564 if name in self.query_params:
565 self.query_params.remove(name)
566
567
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500568def createMethod(methodName, methodDesc, rootDesc, schema):
569 """Creates a method for attaching to a Resource.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400570
571 Args:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500572 methodName: string, name of the method to use.
573 methodDesc: object, fragment of deserialized discovery document that
574 describes the method.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400575 rootDesc: object, the entire deserialized discovery document.
576 schema: object, mapping of schema names to schema descriptions.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400577 """
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500578 methodName = fix_method_name(methodName)
Daniel Hermesc2113242013-02-27 10:16:13 -0800579 (pathUrl, httpMethod, methodId, accept,
580 maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400581
Daniel Hermes954e1242013-02-28 09:28:37 -0800582 parameters = ResourceMethodParameters(methodDesc)
ade@google.com850cf552010-08-20 23:24:56 +0100583
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500584 def method(self, **kwargs):
585 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorioca876e42011-02-22 19:39:42 -0500586
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500587 for name in kwargs.iterkeys():
Daniel Hermes954e1242013-02-28 09:28:37 -0800588 if name not in parameters.argmap:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500589 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorioca876e42011-02-22 19:39:42 -0500590
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500591 # Remove args that have a value of None.
592 keys = kwargs.keys()
593 for name in keys:
594 if kwargs[name] is None:
595 del kwargs[name]
Joe Gregorio21f11672010-08-18 17:23:17 -0400596
Daniel Hermes954e1242013-02-28 09:28:37 -0800597 for name in parameters.required_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500598 if name not in kwargs:
599 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400600
Daniel Hermes954e1242013-02-28 09:28:37 -0800601 for name, regex in parameters.pattern_params.iteritems():
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500602 if name in kwargs:
603 if isinstance(kwargs[name], basestring):
604 pvalues = [kwargs[name]]
Joe Gregorio61d7e962011-02-22 22:52:07 -0500605 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500606 pvalues = kwargs[name]
607 for pvalue in pvalues:
608 if re.match(regex, pvalue) is None:
609 raise TypeError(
610 'Parameter "%s" value "%s" does not match the pattern "%s"' %
611 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400612
Daniel Hermes954e1242013-02-28 09:28:37 -0800613 for name, enums in parameters.enum_params.iteritems():
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500614 if name in kwargs:
615 # We need to handle the case of a repeated enum
616 # name differently, since we want to handle both
617 # arg='value' and arg=['value1', 'value2']
Daniel Hermes954e1242013-02-28 09:28:37 -0800618 if (name in parameters.repeated_params and
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500619 not isinstance(kwargs[name], basestring)):
620 values = kwargs[name]
621 else:
622 values = [kwargs[name]]
623 for value in values:
624 if value not in enums:
625 raise TypeError(
626 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
627 (name, value, str(enums)))
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400628
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500629 actual_query_params = {}
630 actual_path_params = {}
631 for key, value in kwargs.iteritems():
Daniel Hermes954e1242013-02-28 09:28:37 -0800632 to_type = parameters.param_types.get(key, 'string')
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500633 # For repeated parameters we cast each member of the list.
Daniel Hermes954e1242013-02-28 09:28:37 -0800634 if key in parameters.repeated_params and type(value) == type([]):
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500635 cast_value = [_cast(x, to_type) for x in value]
636 else:
637 cast_value = _cast(value, to_type)
Daniel Hermes954e1242013-02-28 09:28:37 -0800638 if key in parameters.query_params:
639 actual_query_params[parameters.argmap[key]] = cast_value
640 if key in parameters.path_params:
641 actual_path_params[parameters.argmap[key]] = cast_value
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500642 body_value = kwargs.get('body', None)
643 media_filename = kwargs.get('media_body', None)
Joe Gregorioe08a1662011-12-07 09:48:22 -0500644
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500645 if self._developerKey:
646 actual_query_params['key'] = self._developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400647
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500648 model = self._model
649 if methodName.endswith('_media'):
650 model = MediaModel()
651 elif 'response' not in methodDesc:
652 model = RawModel()
653
654 headers = {}
655 headers, params, query, body = model.request(headers,
656 actual_path_params, actual_query_params, body_value)
657
658 expanded_url = uritemplate.expand(pathUrl, params)
659 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
660
661 resumable = None
662 multipart_boundary = ''
663
664 if media_filename:
665 # Ensure we end up with a valid MediaUpload object.
666 if isinstance(media_filename, basestring):
667 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
668 if media_mime_type is None:
669 raise UnknownFileType(media_filename)
670 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
671 raise UnacceptableMimeTypeError(media_mime_type)
672 media_upload = MediaFileUpload(media_filename,
673 mimetype=media_mime_type)
674 elif isinstance(media_filename, MediaUpload):
675 media_upload = media_filename
676 else:
677 raise TypeError('media_filename must be str or MediaUpload.')
678
679 # Check the maxSize
680 if maxSize > 0 and media_upload.size() > maxSize:
681 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
682
683 # Use the media path uri for media uploads
684 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400685 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500686 if media_upload.resumable():
687 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400688
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500689 if media_upload.resumable():
690 # This is all we need to do for resumable, if the body exists it gets
691 # sent in the first request, otherwise an empty body is sent.
692 resumable = media_upload
Joe Gregorio2b781282011-12-08 12:00:25 -0500693 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500694 # A non-resumable upload
695 if body is None:
696 # This is a simple media upload
697 headers['content-type'] = media_upload.mimetype()
698 body = media_upload.getbytes(0, media_upload.size())
699 url = _add_query_parameter(url, 'uploadType', 'media')
700 else:
701 # This is a multipart/related upload.
702 msgRoot = MIMEMultipart('related')
703 # msgRoot should not write out it's own headers
704 setattr(msgRoot, '_write_headers', lambda self: None)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400705
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500706 # attach the body as one part
707 msg = MIMENonMultipart(*headers['content-type'].split('/'))
708 msg.set_payload(body)
709 msgRoot.attach(msg)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400710
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500711 # attach the media as the second part
712 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
713 msg['Content-Transfer-Encoding'] = 'binary'
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400714
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500715 payload = media_upload.getbytes(0, media_upload.size())
716 msg.set_payload(payload)
717 msgRoot.attach(msg)
718 body = msgRoot.as_string()
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400719
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500720 multipart_boundary = msgRoot.get_boundary()
721 headers['content-type'] = ('multipart/related; '
722 'boundary="%s"') % multipart_boundary
723 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400724
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500725 logger.info('URL being requested: %s' % url)
726 return self._requestBuilder(self._http,
727 model.response,
728 url,
729 method=httpMethod,
730 body=body,
731 headers=headers,
732 methodId=methodId,
733 resumable=resumable)
734
735 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
Daniel Hermes954e1242013-02-28 09:28:37 -0800736 if len(parameters.argmap) > 0:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500737 docs.append('Args:\n')
738
739 # Skip undocumented params and params common to all methods.
740 skip_parameters = rootDesc.get('parameters', {}).keys()
741 skip_parameters.extend(STACK_QUERY_PARAMETERS)
742
Daniel Hermes954e1242013-02-28 09:28:37 -0800743 all_args = parameters.argmap.keys()
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500744 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
745
746 # Move body to the front of the line.
747 if 'body' in all_args:
748 args_ordered.append('body')
749
750 for name in all_args:
751 if name not in args_ordered:
752 args_ordered.append(name)
753
754 for arg in args_ordered:
755 if arg in skip_parameters:
756 continue
757
758 repeated = ''
Daniel Hermes954e1242013-02-28 09:28:37 -0800759 if arg in parameters.repeated_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500760 repeated = ' (repeated)'
761 required = ''
Daniel Hermes954e1242013-02-28 09:28:37 -0800762 if arg in parameters.required_params:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500763 required = ' (required)'
Daniel Hermes954e1242013-02-28 09:28:37 -0800764 paramdesc = methodDesc['parameters'][parameters.argmap[arg]]
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500765 paramdoc = paramdesc.get('description', 'A parameter')
766 if '$ref' in paramdesc:
767 docs.append(
768 (' %s: object, %s%s%s\n The object takes the'
769 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
770 schema.prettyPrintByName(paramdesc['$ref'])))
771 else:
772 paramtype = paramdesc.get('type', 'string')
773 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
774 repeated))
775 enum = paramdesc.get('enum', [])
776 enumDesc = paramdesc.get('enumDescriptions', [])
777 if enum and enumDesc:
778 docs.append(' Allowed values\n')
779 for (name, desc) in zip(enum, enumDesc):
780 docs.append(' %s - %s\n' % (name, desc))
781 if 'response' in methodDesc:
782 if methodName.endswith('_media'):
783 docs.append('\nReturns:\n The media object as a string.\n\n ')
784 else:
785 docs.append('\nReturns:\n An object of the form:\n\n ')
786 docs.append(schema.prettyPrintSchema(methodDesc['response']))
787
788 setattr(method, '__doc__', ''.join(docs))
789 return (methodName, method)
790
791
792def createNextMethod(methodName):
793 """Creates any _next methods for attaching to a Resource.
794
795 The _next methods allow for easy iteration through list() responses.
796
797 Args:
798 methodName: string, name of the method to use.
799 """
800 methodName = fix_method_name(methodName)
801
802 def methodNext(self, previous_request, previous_response):
803 """Retrieves the next page of results.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400804
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400805Args:
806 previous_request: The request for the previous page. (required)
807 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400808
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400809Returns:
810 A request object that you can call 'execute()' on to request the next
811 page. Returns None if there are no more items in the collection.
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500812 """
813 # Retrieve nextPageToken from previous_response
814 # Use as pageToken in previous_request to create new request.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400815
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500816 if 'nextPageToken' not in previous_response:
817 return None
Joe Gregorio3c676f92011-07-25 10:38:14 -0400818
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500819 request = copy.copy(previous_request)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400820
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500821 pageToken = previous_response['nextPageToken']
822 parsed = list(urlparse.urlparse(request.uri))
823 q = parse_qsl(parsed[4])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400824
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500825 # Find and remove old 'pageToken' value from URI
826 newq = [(key, value) for (key, value) in q if key != 'pageToken']
827 newq.append(('pageToken', pageToken))
828 parsed[4] = urllib.urlencode(newq)
829 uri = urlparse.urlunparse(parsed)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400830
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500831 request.uri = uri
Joe Gregorio3c676f92011-07-25 10:38:14 -0400832
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500833 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400834
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500835 return request
Joe Gregorio3c676f92011-07-25 10:38:14 -0400836
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500837 return (methodName, methodNext)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400838
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400839
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500840class Resource(object):
841 """A class for interacting with a resource."""
Joe Gregorioaf276d22010-12-09 14:26:58 -0500842
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500843 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
844 resourceDesc, rootDesc, schema):
845 """Build a Resource from the API description.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400846
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500847 Args:
848 http: httplib2.Http, Object to make http requests with.
849 baseUrl: string, base URL for the API. All requests are relative to this
850 URI.
851 model: apiclient.Model, converts to and from the wire format.
852 requestBuilder: class or callable that instantiates an
853 apiclient.HttpRequest object.
854 developerKey: string, key obtained from
855 https://code.google.com/apis/console
856 resourceDesc: object, section of deserialized discovery document that
857 describes a resource. Note that the top level discovery document
858 is considered a resource.
859 rootDesc: object, the entire deserialized discovery document.
860 schema: object, mapping of schema names to schema descriptions.
861 """
862 self._dynamic_attrs = []
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400863
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500864 self._http = http
865 self._baseUrl = baseUrl
866 self._model = model
867 self._developerKey = developerKey
868 self._requestBuilder = requestBuilder
869 self._resourceDesc = resourceDesc
870 self._rootDesc = rootDesc
871 self._schema = schema
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400872
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500873 self._set_service_methods()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400874
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500875 def _set_dynamic_attr(self, attr_name, value):
876 """Sets an instance attribute and tracks it in a list of dynamic attributes.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400877
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500878 Args:
879 attr_name: string; The name of the attribute to be set
880 value: The value being set on the object and tracked in the dynamic cache.
881 """
882 self._dynamic_attrs.append(attr_name)
883 self.__dict__[attr_name] = value
Joe Gregorio48d361f2010-08-18 13:19:21 -0400884
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500885 def __getstate__(self):
886 """Trim the state down to something that can be pickled.
887
888 Uses the fact that the instance variable _dynamic_attrs holds attrs that
889 will be wiped and restored on pickle serialization.
890 """
891 state_dict = copy.copy(self.__dict__)
892 for dynamic_attr in self._dynamic_attrs:
893 del state_dict[dynamic_attr]
894 del state_dict['_dynamic_attrs']
895 return state_dict
896
897 def __setstate__(self, state):
898 """Reconstitute the state of the object from being pickled.
899
900 Uses the fact that the instance variable _dynamic_attrs holds attrs that
901 will be wiped and restored on pickle serialization.
902 """
903 self.__dict__.update(state)
904 self._dynamic_attrs = []
905 self._set_service_methods()
906
907 def _set_service_methods(self):
908 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
909 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
910 self._add_next_methods(self._resourceDesc, self._schema)
911
912 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
913 # Add basic methods to Resource
914 if 'methods' in resourceDesc:
915 for methodName, methodDesc in resourceDesc['methods'].iteritems():
916 fixedMethodName, method = createMethod(
917 methodName, methodDesc, rootDesc, schema)
918 self._set_dynamic_attr(fixedMethodName,
919 method.__get__(self, self.__class__))
920 # Add in _media methods. The functionality of the attached method will
921 # change when it sees that the method name ends in _media.
922 if methodDesc.get('supportsMediaDownload', False):
923 fixedMethodName, method = createMethod(
924 methodName + '_media', methodDesc, rootDesc, schema)
925 self._set_dynamic_attr(fixedMethodName,
926 method.__get__(self, self.__class__))
927
928 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
929 # Add in nested resources
930 if 'resources' in resourceDesc:
931
932 def createResourceMethod(methodName, methodDesc):
933 """Create a method on the Resource to access a nested Resource.
934
935 Args:
936 methodName: string, name of the method to use.
937 methodDesc: object, fragment of deserialized discovery document that
938 describes the method.
939 """
940 methodName = fix_method_name(methodName)
941
942 def methodResource(self):
943 return Resource(http=self._http, baseUrl=self._baseUrl,
944 model=self._model, developerKey=self._developerKey,
945 requestBuilder=self._requestBuilder,
946 resourceDesc=methodDesc, rootDesc=rootDesc,
947 schema=schema)
948
949 setattr(methodResource, '__doc__', 'A collection resource.')
950 setattr(methodResource, '__is_resource__', True)
951
952 return (methodName, methodResource)
953
954 for methodName, methodDesc in resourceDesc['resources'].iteritems():
955 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
956 self._set_dynamic_attr(fixedMethodName,
957 method.__get__(self, self.__class__))
958
959 def _add_next_methods(self, resourceDesc, schema):
960 # Add _next() methods
961 # Look for response bodies in schema that contain nextPageToken, and methods
962 # that take a pageToken parameter.
963 if 'methods' in resourceDesc:
964 for methodName, methodDesc in resourceDesc['methods'].iteritems():
965 if 'response' in methodDesc:
966 responseSchema = methodDesc['response']
967 if '$ref' in responseSchema:
968 responseSchema = schema.get(responseSchema['$ref'])
969 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
970 {})
971 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
972 if hasNextPageToken and hasPageToken:
973 fixedMethodName, method = createNextMethod(methodName + '_next')
974 self._set_dynamic_attr(fixedMethodName,
975 method.__get__(self, self.__class__))