blob: c31d65ec46361ed4789d06ab25791aed6b0bb575 [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
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500479def createMethod(methodName, methodDesc, rootDesc, schema):
480 """Creates a method for attaching to a Resource.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400481
482 Args:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500483 methodName: string, name of the method to use.
484 methodDesc: object, fragment of deserialized discovery document that
485 describes the method.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400486 rootDesc: object, the entire deserialized discovery document.
487 schema: object, mapping of schema names to schema descriptions.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400488 """
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500489 methodName = fix_method_name(methodName)
Daniel Hermesc2113242013-02-27 10:16:13 -0800490 (pathUrl, httpMethod, methodId, accept,
491 maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400492
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500493 argmap = {} # Map from method parameter name to query parameter name
494 required_params = [] # Required parameters
495 repeated_params = [] # Repeated parameters
496 pattern_params = {} # Parameters that must match a regex
497 query_params = [] # Parameters that will be used in the query string
498 path_params = {} # Parameters that will be used in the base URL
499 param_type = {} # The type of the parameter
500 enum_params = {} # Allowable enumeration values for each parameter
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400501
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500502 if 'parameters' in methodDesc:
503 for arg, desc in methodDesc['parameters'].iteritems():
504 param = key2param(arg)
505 argmap[param] = arg
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400506
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500507 if desc.get('pattern', ''):
508 pattern_params[param] = desc['pattern']
509 if desc.get('enum', ''):
510 enum_params[param] = desc['enum']
511 if desc.get('required', False):
512 required_params.append(param)
513 if desc.get('repeated', False):
514 repeated_params.append(param)
515 if desc.get('location') == 'query':
516 query_params.append(param)
517 if desc.get('location') == 'path':
518 path_params[param] = param
519 param_type[param] = desc.get('type', 'string')
Joe Gregorioca876e42011-02-22 19:39:42 -0500520
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500521 for match in URITEMPLATE.finditer(pathUrl):
522 for namematch in VARNAME.finditer(match.group(0)):
523 name = key2param(namematch.group(0))
524 path_params[name] = name
525 if name in query_params:
526 query_params.remove(name)
ade@google.com850cf552010-08-20 23:24:56 +0100527
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500528 def method(self, **kwargs):
529 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorioca876e42011-02-22 19:39:42 -0500530
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500531 for name in kwargs.iterkeys():
532 if name not in argmap:
533 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorioca876e42011-02-22 19:39:42 -0500534
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500535 # Remove args that have a value of None.
536 keys = kwargs.keys()
537 for name in keys:
538 if kwargs[name] is None:
539 del kwargs[name]
Joe Gregorio21f11672010-08-18 17:23:17 -0400540
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500541 for name in required_params:
542 if name not in kwargs:
543 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400544
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500545 for name, regex in pattern_params.iteritems():
546 if name in kwargs:
547 if isinstance(kwargs[name], basestring):
548 pvalues = [kwargs[name]]
Joe Gregorio61d7e962011-02-22 22:52:07 -0500549 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500550 pvalues = kwargs[name]
551 for pvalue in pvalues:
552 if re.match(regex, pvalue) is None:
553 raise TypeError(
554 'Parameter "%s" value "%s" does not match the pattern "%s"' %
555 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400556
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500557 for name, enums in enum_params.iteritems():
558 if name in kwargs:
559 # We need to handle the case of a repeated enum
560 # name differently, since we want to handle both
561 # arg='value' and arg=['value1', 'value2']
562 if (name in repeated_params and
563 not isinstance(kwargs[name], basestring)):
564 values = kwargs[name]
565 else:
566 values = [kwargs[name]]
567 for value in values:
568 if value not in enums:
569 raise TypeError(
570 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
571 (name, value, str(enums)))
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400572
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500573 actual_query_params = {}
574 actual_path_params = {}
575 for key, value in kwargs.iteritems():
576 to_type = param_type.get(key, 'string')
577 # For repeated parameters we cast each member of the list.
578 if key in repeated_params and type(value) == type([]):
579 cast_value = [_cast(x, to_type) for x in value]
580 else:
581 cast_value = _cast(value, to_type)
582 if key in query_params:
583 actual_query_params[argmap[key]] = cast_value
584 if key in path_params:
585 actual_path_params[argmap[key]] = cast_value
586 body_value = kwargs.get('body', None)
587 media_filename = kwargs.get('media_body', None)
Joe Gregorioe08a1662011-12-07 09:48:22 -0500588
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500589 if self._developerKey:
590 actual_query_params['key'] = self._developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400591
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500592 model = self._model
593 if methodName.endswith('_media'):
594 model = MediaModel()
595 elif 'response' not in methodDesc:
596 model = RawModel()
597
598 headers = {}
599 headers, params, query, body = model.request(headers,
600 actual_path_params, actual_query_params, body_value)
601
602 expanded_url = uritemplate.expand(pathUrl, params)
603 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
604
605 resumable = None
606 multipart_boundary = ''
607
608 if media_filename:
609 # Ensure we end up with a valid MediaUpload object.
610 if isinstance(media_filename, basestring):
611 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
612 if media_mime_type is None:
613 raise UnknownFileType(media_filename)
614 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
615 raise UnacceptableMimeTypeError(media_mime_type)
616 media_upload = MediaFileUpload(media_filename,
617 mimetype=media_mime_type)
618 elif isinstance(media_filename, MediaUpload):
619 media_upload = media_filename
620 else:
621 raise TypeError('media_filename must be str or MediaUpload.')
622
623 # Check the maxSize
624 if maxSize > 0 and media_upload.size() > maxSize:
625 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
626
627 # Use the media path uri for media uploads
628 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400629 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500630 if media_upload.resumable():
631 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400632
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500633 if media_upload.resumable():
634 # This is all we need to do for resumable, if the body exists it gets
635 # sent in the first request, otherwise an empty body is sent.
636 resumable = media_upload
Joe Gregorio2b781282011-12-08 12:00:25 -0500637 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500638 # A non-resumable upload
639 if body is None:
640 # This is a simple media upload
641 headers['content-type'] = media_upload.mimetype()
642 body = media_upload.getbytes(0, media_upload.size())
643 url = _add_query_parameter(url, 'uploadType', 'media')
644 else:
645 # This is a multipart/related upload.
646 msgRoot = MIMEMultipart('related')
647 # msgRoot should not write out it's own headers
648 setattr(msgRoot, '_write_headers', lambda self: None)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400649
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500650 # attach the body as one part
651 msg = MIMENonMultipart(*headers['content-type'].split('/'))
652 msg.set_payload(body)
653 msgRoot.attach(msg)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400654
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500655 # attach the media as the second part
656 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
657 msg['Content-Transfer-Encoding'] = 'binary'
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400658
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500659 payload = media_upload.getbytes(0, media_upload.size())
660 msg.set_payload(payload)
661 msgRoot.attach(msg)
662 body = msgRoot.as_string()
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400663
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500664 multipart_boundary = msgRoot.get_boundary()
665 headers['content-type'] = ('multipart/related; '
666 'boundary="%s"') % multipart_boundary
667 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400668
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500669 logger.info('URL being requested: %s' % url)
670 return self._requestBuilder(self._http,
671 model.response,
672 url,
673 method=httpMethod,
674 body=body,
675 headers=headers,
676 methodId=methodId,
677 resumable=resumable)
678
679 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
680 if len(argmap) > 0:
681 docs.append('Args:\n')
682
683 # Skip undocumented params and params common to all methods.
684 skip_parameters = rootDesc.get('parameters', {}).keys()
685 skip_parameters.extend(STACK_QUERY_PARAMETERS)
686
687 all_args = argmap.keys()
688 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
689
690 # Move body to the front of the line.
691 if 'body' in all_args:
692 args_ordered.append('body')
693
694 for name in all_args:
695 if name not in args_ordered:
696 args_ordered.append(name)
697
698 for arg in args_ordered:
699 if arg in skip_parameters:
700 continue
701
702 repeated = ''
703 if arg in repeated_params:
704 repeated = ' (repeated)'
705 required = ''
706 if arg in required_params:
707 required = ' (required)'
708 paramdesc = methodDesc['parameters'][argmap[arg]]
709 paramdoc = paramdesc.get('description', 'A parameter')
710 if '$ref' in paramdesc:
711 docs.append(
712 (' %s: object, %s%s%s\n The object takes the'
713 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
714 schema.prettyPrintByName(paramdesc['$ref'])))
715 else:
716 paramtype = paramdesc.get('type', 'string')
717 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
718 repeated))
719 enum = paramdesc.get('enum', [])
720 enumDesc = paramdesc.get('enumDescriptions', [])
721 if enum and enumDesc:
722 docs.append(' Allowed values\n')
723 for (name, desc) in zip(enum, enumDesc):
724 docs.append(' %s - %s\n' % (name, desc))
725 if 'response' in methodDesc:
726 if methodName.endswith('_media'):
727 docs.append('\nReturns:\n The media object as a string.\n\n ')
728 else:
729 docs.append('\nReturns:\n An object of the form:\n\n ')
730 docs.append(schema.prettyPrintSchema(methodDesc['response']))
731
732 setattr(method, '__doc__', ''.join(docs))
733 return (methodName, method)
734
735
736def createNextMethod(methodName):
737 """Creates any _next methods for attaching to a Resource.
738
739 The _next methods allow for easy iteration through list() responses.
740
741 Args:
742 methodName: string, name of the method to use.
743 """
744 methodName = fix_method_name(methodName)
745
746 def methodNext(self, previous_request, previous_response):
747 """Retrieves the next page of results.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400748
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400749Args:
750 previous_request: The request for the previous page. (required)
751 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400752
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400753Returns:
754 A request object that you can call 'execute()' on to request the next
755 page. Returns None if there are no more items in the collection.
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500756 """
757 # Retrieve nextPageToken from previous_response
758 # Use as pageToken in previous_request to create new request.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400759
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500760 if 'nextPageToken' not in previous_response:
761 return None
Joe Gregorio3c676f92011-07-25 10:38:14 -0400762
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500763 request = copy.copy(previous_request)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400764
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500765 pageToken = previous_response['nextPageToken']
766 parsed = list(urlparse.urlparse(request.uri))
767 q = parse_qsl(parsed[4])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400768
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500769 # Find and remove old 'pageToken' value from URI
770 newq = [(key, value) for (key, value) in q if key != 'pageToken']
771 newq.append(('pageToken', pageToken))
772 parsed[4] = urllib.urlencode(newq)
773 uri = urlparse.urlunparse(parsed)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400774
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500775 request.uri = uri
Joe Gregorio3c676f92011-07-25 10:38:14 -0400776
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500777 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400778
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500779 return request
Joe Gregorio3c676f92011-07-25 10:38:14 -0400780
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500781 return (methodName, methodNext)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400782
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400783
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500784class Resource(object):
785 """A class for interacting with a resource."""
Joe Gregorioaf276d22010-12-09 14:26:58 -0500786
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500787 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
788 resourceDesc, rootDesc, schema):
789 """Build a Resource from the API description.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400790
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500791 Args:
792 http: httplib2.Http, Object to make http requests with.
793 baseUrl: string, base URL for the API. All requests are relative to this
794 URI.
795 model: apiclient.Model, converts to and from the wire format.
796 requestBuilder: class or callable that instantiates an
797 apiclient.HttpRequest object.
798 developerKey: string, key obtained from
799 https://code.google.com/apis/console
800 resourceDesc: object, section of deserialized discovery document that
801 describes a resource. Note that the top level discovery document
802 is considered a resource.
803 rootDesc: object, the entire deserialized discovery document.
804 schema: object, mapping of schema names to schema descriptions.
805 """
806 self._dynamic_attrs = []
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400807
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500808 self._http = http
809 self._baseUrl = baseUrl
810 self._model = model
811 self._developerKey = developerKey
812 self._requestBuilder = requestBuilder
813 self._resourceDesc = resourceDesc
814 self._rootDesc = rootDesc
815 self._schema = schema
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400816
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500817 self._set_service_methods()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400818
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500819 def _set_dynamic_attr(self, attr_name, value):
820 """Sets an instance attribute and tracks it in a list of dynamic attributes.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400821
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500822 Args:
823 attr_name: string; The name of the attribute to be set
824 value: The value being set on the object and tracked in the dynamic cache.
825 """
826 self._dynamic_attrs.append(attr_name)
827 self.__dict__[attr_name] = value
Joe Gregorio48d361f2010-08-18 13:19:21 -0400828
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500829 def __getstate__(self):
830 """Trim the state down to something that can be pickled.
831
832 Uses the fact that the instance variable _dynamic_attrs holds attrs that
833 will be wiped and restored on pickle serialization.
834 """
835 state_dict = copy.copy(self.__dict__)
836 for dynamic_attr in self._dynamic_attrs:
837 del state_dict[dynamic_attr]
838 del state_dict['_dynamic_attrs']
839 return state_dict
840
841 def __setstate__(self, state):
842 """Reconstitute the state of the object from being pickled.
843
844 Uses the fact that the instance variable _dynamic_attrs holds attrs that
845 will be wiped and restored on pickle serialization.
846 """
847 self.__dict__.update(state)
848 self._dynamic_attrs = []
849 self._set_service_methods()
850
851 def _set_service_methods(self):
852 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
853 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
854 self._add_next_methods(self._resourceDesc, self._schema)
855
856 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
857 # Add basic methods to Resource
858 if 'methods' in resourceDesc:
859 for methodName, methodDesc in resourceDesc['methods'].iteritems():
860 fixedMethodName, method = createMethod(
861 methodName, methodDesc, rootDesc, schema)
862 self._set_dynamic_attr(fixedMethodName,
863 method.__get__(self, self.__class__))
864 # Add in _media methods. The functionality of the attached method will
865 # change when it sees that the method name ends in _media.
866 if methodDesc.get('supportsMediaDownload', False):
867 fixedMethodName, method = createMethod(
868 methodName + '_media', methodDesc, rootDesc, schema)
869 self._set_dynamic_attr(fixedMethodName,
870 method.__get__(self, self.__class__))
871
872 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
873 # Add in nested resources
874 if 'resources' in resourceDesc:
875
876 def createResourceMethod(methodName, methodDesc):
877 """Create a method on the Resource to access a nested Resource.
878
879 Args:
880 methodName: string, name of the method to use.
881 methodDesc: object, fragment of deserialized discovery document that
882 describes the method.
883 """
884 methodName = fix_method_name(methodName)
885
886 def methodResource(self):
887 return Resource(http=self._http, baseUrl=self._baseUrl,
888 model=self._model, developerKey=self._developerKey,
889 requestBuilder=self._requestBuilder,
890 resourceDesc=methodDesc, rootDesc=rootDesc,
891 schema=schema)
892
893 setattr(methodResource, '__doc__', 'A collection resource.')
894 setattr(methodResource, '__is_resource__', True)
895
896 return (methodName, methodResource)
897
898 for methodName, methodDesc in resourceDesc['resources'].iteritems():
899 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
900 self._set_dynamic_attr(fixedMethodName,
901 method.__get__(self, self.__class__))
902
903 def _add_next_methods(self, resourceDesc, schema):
904 # Add _next() methods
905 # Look for response bodies in schema that contain nextPageToken, and methods
906 # that take a pageToken parameter.
907 if 'methods' in resourceDesc:
908 for methodName, methodDesc in resourceDesc['methods'].iteritems():
909 if 'response' in methodDesc:
910 responseSchema = methodDesc['response']
911 if '$ref' in responseSchema:
912 responseSchema = schema.get(responseSchema['$ref'])
913 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
914 {})
915 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
916 if hasNextPageToken and hasPageToken:
917 fixedMethodName, method = createNextMethod(methodName + '_next')
918 self._set_dynamic_attr(fixedMethodName,
919 method.__get__(self, self.__class__))