blob: cf835300511db6f3561dd413331d1aedcf691ed1 [file] [log] [blame]
Joe Gregorio48d361f2010-08-18 13:19:21 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Client for discovery based APIs
16
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017A client library for Google's discovery based APIs.
Joe Gregorio48d361f2010-08-18 13:19:21 -040018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorioabda96f2011-02-11 20:19:33 -050021__all__ = [
Joe Gregorioce31a972012-06-06 15:48:17 -040022 'build',
23 'build_from_document'
24 'fix_method_name',
25 '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
ade@google.com850cf552010-08-20 23:24:56 +010030import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040031import os
Joe Gregoriod0bd3882011-11-22 09:49:47 -050032import random
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:
41 from urlparse import parse_qsl
42except ImportError:
43 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
50from apiclient.errors import UnknownLinkType
51from 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 Gregorio549230c2012-01-11 10:38:05 -050060from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050061
Joe Gregorioe84c9442012-03-12 08:45:57 -040062logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040063
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050064URITEMPLATE = re.compile('{[^}]*}')
65VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040066DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
67 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050068DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050069
Joe Gregorioc8e421c2012-06-06 14:03:13 -040070# Parameters accepted by the stack, but not visible via discovery.
71STACK_QUERY_PARAMETERS = ['trace', 'pp', 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040072
Joe Gregorioc8e421c2012-06-06 14:03:13 -040073# Python reserved words.
Joe Gregorio562b7312011-09-15 09:06:38 -040074RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040075 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
76 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
Joe Gregorio81d92cc2012-07-09 16:46:02 -040077 'pass', 'print', 'raise', 'return', 'try', 'while', 'body']
Joe Gregoriod92897c2011-07-07 11:44:56 -040078
Joe Gregorio562b7312011-09-15 09:06:38 -040079
Joe Gregorioce31a972012-06-06 15:48:17 -040080def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040081 """Fix method names to avoid reserved word conflicts.
82
83 Args:
84 name: string, method name.
85
86 Returns:
87 The name with a '_' prefixed if the name is a reserved word.
88 """
Joe Gregoriod92897c2011-07-07 11:44:56 -040089 if name in RESERVED_WORDS:
90 return name + '_'
91 else:
92 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040093
Joe Gregorioa98733f2011-09-16 10:12:28 -040094
Joe Gregorioa98733f2011-09-16 10:12:28 -040095def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050096 """Adds a query parameter to a url.
97
98 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -040099
100 Args:
101 url: string, url to add the query parameter to.
102 name: string, query parameter name.
103 value: string, query parameter value.
104
105 Returns:
106 Updated query parameter. Does not update the url if value is None.
107 """
108 if value is None:
109 return url
110 else:
111 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500112 q = dict(parse_qsl(parsed[4]))
113 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400114 parsed[4] = urllib.urlencode(q)
115 return urlparse.urlunparse(parsed)
116
117
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 Gregorio01770a52012-02-24 11:11:10 -0500142def build(serviceName,
143 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500144 http=None,
145 discoveryServiceUrl=DISCOVERY_URI,
146 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500147 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500148 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500149 """Construct a Resource for interacting with an API.
150
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400151 Construct a Resource object for interacting with an API. The serviceName and
152 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500153
154 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400155 serviceName: string, name of the service.
156 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500157 http: httplib2.Http, An instance of httplib2.Http or something that acts
158 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400159 discoveryServiceUrl: string, a URI Template that points to the location of
160 the discovery service. It should have two parameters {api} and
161 {apiVersion} that when filled in produce an absolute URI to the discovery
162 document for that service.
163 developerKey: string, key obtained from
164 https://code.google.com/apis/console.
165 model: apiclient.Model, converts to and from the wire format.
166 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
167 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500168
169 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400170 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500171 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400173 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400174 'apiVersion': version
175 }
ade@google.com850cf552010-08-20 23:24:56 +0100176
Joe Gregorioc204b642010-09-21 12:01:23 -0400177 if http is None:
178 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400179
ade@google.com850cf552010-08-20 23:24:56 +0100180 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400181
Joe Gregorio66f57522011-11-30 11:00:00 -0500182 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
183 # variable that contains the network address of the client sending the
184 # request. If it exists then add that to the request for the discovery
185 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400186 if 'REMOTE_ADDR' in os.environ:
187 requested_url = _add_query_parameter(requested_url, 'userIp',
188 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400189 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400190
ade@google.com850cf552010-08-20 23:24:56 +0100191 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400192
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500193 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500194 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500195 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400196 if resp.status >= 400:
Joe Gregorio49396552011-03-08 10:39:00 -0500197 raise HttpError(resp, content, requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400198
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500199 try:
200 service = simplejson.loads(content)
201 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400202 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500203 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400204
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400205 return build_from_document(content, discoveryServiceUrl, http=http,
206 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500207
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500208
Joe Gregorio292b9b82011-01-12 11:36:11 -0500209def build_from_document(
210 service,
Joe Gregorioa2838152012-07-16 11:52:17 -0400211 base=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500212 future=None,
213 http=None,
214 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500215 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500216 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500217 """Create a Resource for interacting with an API.
218
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400219 Same as `build()`, but constructs the Resource object from a discovery
220 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500221
Joe Gregorio292b9b82011-01-12 11:36:11 -0500222 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400223 service: string, discovery document.
224 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400225 This parameter is no longer used as rootUrl and servicePath are included
226 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400227 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500228 http: httplib2.Http, An instance of httplib2.Http or something that acts
229 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500230 developerKey: string, Key for controlling API usage, generated
231 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400232 model: Model class instance that serializes and de-serializes requests and
233 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500234 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500235
236 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400237 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500238 """
239
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400240 # future is no longer used.
241 future = {}
242
Joe Gregorio292b9b82011-01-12 11:36:11 -0500243 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400244 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500245 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400246
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500247 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500248 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500249 model = JsonModel('dataWrapper' in features)
Joe Gregorioebd0b842012-06-15 14:14:17 -0400250 resource = _createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400251 service, service, schema)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400252
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500253 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400254
255
Joe Gregorio61d7e962011-02-22 22:52:07 -0500256def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500257 """Convert value to a string based on JSON Schema type.
258
259 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
260 JSON Schema.
261
262 Args:
263 value: any, the value to convert
264 schema_type: string, the type that value should be interpreted as
265
266 Returns:
267 A string representation of 'value' based on the schema_type.
268 """
269 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500270 if type(value) == type('') or type(value) == type(u''):
271 return value
272 else:
273 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500274 elif schema_type == 'integer':
275 return str(int(value))
276 elif schema_type == 'number':
277 return str(float(value))
278 elif schema_type == 'boolean':
279 return str(bool(value)).lower()
280 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500281 if type(value) == type('') or type(value) == type(u''):
282 return value
283 else:
284 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500285
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400286
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400287MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400288 "KB": 2 ** 10,
289 "MB": 2 ** 20,
290 "GB": 2 ** 30,
291 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400292 }
293
Joe Gregorioa98733f2011-09-16 10:12:28 -0400294
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400295def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400296 """Convert a string media size, such as 10GB or 3TB into an integer.
297
298 Args:
299 maxSize: string, size as a string, such as 2MB or 7GB.
300
301 Returns:
302 The size as an integer value.
303 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400304 if len(maxSize) < 2:
305 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400306 units = maxSize[-2:].upper()
307 multiplier = MULTIPLIERS.get(units, 0)
308 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400309 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400310 else:
311 return int(maxSize)
312
Joe Gregoriobee86832011-02-22 10:00:19 -0500313
Joe Gregorioebd0b842012-06-15 14:14:17 -0400314def _createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400315 developerKey, resourceDesc, rootDesc, schema):
316 """Build a Resource from the API description.
317
318 Args:
319 http: httplib2.Http, Object to make http requests with.
320 baseUrl: string, base URL for the API. All requests are relative to this
321 URI.
322 model: apiclient.Model, converts to and from the wire format.
323 requestBuilder: class or callable that instantiates an
324 apiclient.HttpRequest object.
325 developerKey: string, key obtained from
326 https://code.google.com/apis/console
327 resourceDesc: object, section of deserialized discovery document that
328 describes a resource. Note that the top level discovery document
329 is considered a resource.
330 rootDesc: object, the entire deserialized discovery document.
331 schema: object, mapping of schema names to schema descriptions.
332
333 Returns:
334 An instance of Resource with all the methods attached for interacting with
335 that resource.
336 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400337
338 class Resource(object):
339 """A class for interacting with a resource."""
340
341 def __init__(self):
342 self._http = http
343 self._baseUrl = baseUrl
344 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400345 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500346 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400347
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400348 def createMethod(theclass, methodName, methodDesc, rootDesc):
349 """Creates a method for attaching to a Resource.
350
351 Args:
352 theclass: type, the class to attach methods to.
353 methodName: string, name of the method to use.
354 methodDesc: object, fragment of deserialized discovery document that
355 describes the method.
356 rootDesc: object, the entire deserialized discovery document.
357 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400358 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400359 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400360 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400361 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400362
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400363 mediaPathUrl = None
364 accept = []
365 maxSize = 0
366 if 'mediaUpload' in methodDesc:
367 mediaUpload = methodDesc['mediaUpload']
Joe Gregorioa2838152012-07-16 11:52:17 -0400368 mediaPathUrl = (rootDesc['rootUrl'] + 'upload/' + rootDesc['servicePath']
369 + pathUrl)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400370 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400371 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400372
Joe Gregorioca876e42011-02-22 19:39:42 -0500373 if 'parameters' not in methodDesc:
374 methodDesc['parameters'] = {}
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400375
376 # Add in the parameters common to all methods.
377 for name, desc in rootDesc.get('parameters', {}).iteritems():
378 methodDesc['parameters'][name] = desc
379
380 # Add in undocumented query parameters.
Joe Gregorioca876e42011-02-22 19:39:42 -0500381 for name in STACK_QUERY_PARAMETERS:
382 methodDesc['parameters'][name] = {
383 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400384 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500385 }
386
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500387 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500388 methodDesc['parameters']['body'] = {
389 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500390 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500391 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500392 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500393 if 'request' in methodDesc:
394 methodDesc['parameters']['body'].update(methodDesc['request'])
395 else:
396 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500397 if 'mediaUpload' in methodDesc:
398 methodDesc['parameters']['media_body'] = {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400399 'description':
400 'The filename of the media request body, or an instance of a '
401 'MediaUpload object.',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500402 'type': 'string',
403 'required': False,
404 }
405 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400406 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100407
Joe Gregorioca876e42011-02-22 19:39:42 -0500408 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100409 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500410 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100411 pattern_params = {} # Parameters that must match a regex
412 query_params = [] # Parameters that will be used in the query string
413 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500414 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500415 enum_params = {} # Allowable enumeration values for each parameter
416
417
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400418 if 'parameters' in methodDesc:
419 for arg, desc in methodDesc['parameters'].iteritems():
420 param = key2param(arg)
421 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400422
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400423 if desc.get('pattern', ''):
424 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500425 if desc.get('enum', ''):
426 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400427 if desc.get('required', False):
428 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500429 if desc.get('repeated', False):
430 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400431 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400432 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400433 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400434 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500435 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400436
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500437 for match in URITEMPLATE.finditer(pathUrl):
438 for namematch in VARNAME.finditer(match.group(0)):
439 name = key2param(namematch.group(0))
440 path_params[name] = name
441 if name in query_params:
442 query_params.remove(name)
443
Joe Gregorio48d361f2010-08-18 13:19:21 -0400444 def method(self, **kwargs):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400445 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorio2467afa2012-06-20 12:21:25 -0400446
Joe Gregorio48d361f2010-08-18 13:19:21 -0400447 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500448 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400449 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400450
Joe Gregorio2467afa2012-06-20 12:21:25 -0400451 # Remove args that have a value of None.
452 keys = kwargs.keys()
453 for name in keys:
454 if kwargs[name] is None:
455 del kwargs[name]
456
ade@google.com850cf552010-08-20 23:24:56 +0100457 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400458 if name not in kwargs:
459 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400460
ade@google.com850cf552010-08-20 23:24:56 +0100461 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400462 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500463 if isinstance(kwargs[name], basestring):
464 pvalues = [kwargs[name]]
465 else:
466 pvalues = kwargs[name]
467 for pvalue in pvalues:
468 if re.match(regex, pvalue) is None:
469 raise TypeError(
470 'Parameter "%s" value "%s" does not match the pattern "%s"' %
471 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400472
Joe Gregoriobee86832011-02-22 10:00:19 -0500473 for name, enums in enum_params.iteritems():
474 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800475 # We need to handle the case of a repeated enum
476 # name differently, since we want to handle both
477 # arg='value' and arg=['value1', 'value2']
478 if (name in repeated_params and
479 not isinstance(kwargs[name], basestring)):
480 values = kwargs[name]
481 else:
482 values = [kwargs[name]]
483 for value in values:
484 if value not in enums:
485 raise TypeError(
486 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
487 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500488
ade@google.com850cf552010-08-20 23:24:56 +0100489 actual_query_params = {}
490 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400491 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500492 to_type = param_type.get(key, 'string')
493 # For repeated parameters we cast each member of the list.
494 if key in repeated_params and type(value) == type([]):
495 cast_value = [_cast(x, to_type) for x in value]
496 else:
497 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100498 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500499 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100500 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500501 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100502 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400503 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400504
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400505 if self._developerKey:
506 actual_query_params['key'] = self._developerKey
507
Joe Gregorioe08a1662011-12-07 09:48:22 -0500508 model = self._model
Joe Gregorio708388c2012-06-15 13:43:04 -0400509 if methodName.endswith('_media'):
510 model = MediaModel()
511 elif 'response' not in methodDesc:
Joe Gregorioe08a1662011-12-07 09:48:22 -0500512 model = RawModel()
513
Joe Gregorio48d361f2010-08-18 13:19:21 -0400514 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500515 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400516 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400517
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400518 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400519 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
520
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500521 resumable = None
522 multipart_boundary = ''
523
Joe Gregorio922b78c2011-05-26 21:36:34 -0400524 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500525 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500526 if isinstance(media_filename, basestring):
527 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
528 if media_mime_type is None:
529 raise UnknownFileType(media_filename)
530 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
531 raise UnacceptableMimeTypeError(media_mime_type)
532 media_upload = MediaFileUpload(media_filename, media_mime_type)
533 elif isinstance(media_filename, MediaUpload):
534 media_upload = media_filename
535 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500536 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500537
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400538 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500539 if maxSize > 0 and media_upload.size() > maxSize:
540 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400541
542 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500543 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400544 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500545 if media_upload.resumable():
546 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400547
Joe Gregorio945be3e2012-01-27 17:01:06 -0500548 if media_upload.resumable():
549 # This is all we need to do for resumable, if the body exists it gets
550 # sent in the first request, otherwise an empty body is sent.
551 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400552 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500553 # A non-resumable upload
554 if body is None:
555 # This is a simple media upload
556 headers['content-type'] = media_upload.mimetype()
557 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500558 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500559 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500560 # This is a multipart/related upload.
561 msgRoot = MIMEMultipart('related')
562 # msgRoot should not write out it's own headers
563 setattr(msgRoot, '_write_headers', lambda self: None)
564
565 # attach the body as one part
566 msg = MIMENonMultipart(*headers['content-type'].split('/'))
567 msg.set_payload(body)
568 msgRoot.attach(msg)
569
570 # attach the media as the second part
571 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
572 msg['Content-Transfer-Encoding'] = 'binary'
573
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500574 payload = media_upload.getbytes(0, media_upload.size())
575 msg.set_payload(payload)
576 msgRoot.attach(msg)
577 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400578
Joe Gregorio945be3e2012-01-27 17:01:06 -0500579 multipart_boundary = msgRoot.get_boundary()
580 headers['content-type'] = ('multipart/related; '
581 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500582 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400583
Joe Gregorioe84c9442012-03-12 08:45:57 -0400584 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500585 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500586 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500587 url,
588 method=httpMethod,
589 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500590 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500591 methodId=methodId,
592 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400593
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500594 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
595 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500596 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400597
598 # Skip undocumented params and params common to all methods.
599 skip_parameters = rootDesc.get('parameters', {}).keys()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400600 skip_parameters.extend(STACK_QUERY_PARAMETERS)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400601
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400602 all_args = argmap.keys()
603 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
604
605 # Move body to the front of the line.
606 if 'body' in all_args:
607 args_ordered.append('body')
608
609 for name in all_args:
610 if name not in args_ordered:
611 args_ordered.append(name)
612
613 for arg in args_ordered:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400614 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500615 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400616
Joe Gregorio61d7e962011-02-22 22:52:07 -0500617 repeated = ''
618 if arg in repeated_params:
619 repeated = ' (repeated)'
620 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400621 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500622 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500623 paramdesc = methodDesc['parameters'][argmap[arg]]
624 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500625 if '$ref' in paramdesc:
626 docs.append(
627 (' %s: object, %s%s%s\n The object takes the'
628 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
629 schema.prettyPrintByName(paramdesc['$ref'])))
630 else:
631 paramtype = paramdesc.get('type', 'string')
632 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
633 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500634 enum = paramdesc.get('enum', [])
635 enumDesc = paramdesc.get('enumDescriptions', [])
636 if enum and enumDesc:
637 docs.append(' Allowed values\n')
638 for (name, desc) in zip(enum, enumDesc):
639 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500640 if 'response' in methodDesc:
Joe Gregorio708388c2012-06-15 13:43:04 -0400641 if methodName.endswith('_media'):
642 docs.append('\nReturns:\n The media object as a string.\n\n ')
643 else:
644 docs.append('\nReturns:\n An object of the form:\n\n ')
645 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400646
647 setattr(method, '__doc__', ''.join(docs))
648 setattr(theclass, methodName, method)
649
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400650 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
651 """Creates any _next methods for attaching to a Resource.
652
653 The _next methods allow for easy iteration through list() responses.
654
655 Args:
656 theclass: type, the class to attach methods to.
657 methodName: string, name of the method to use.
658 methodDesc: object, fragment of deserialized discovery document that
659 describes the method.
660 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400661 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400662 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400663 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400664
Joe Gregorio3c676f92011-07-25 10:38:14 -0400665 def methodNext(self, previous_request, previous_response):
666 """Retrieves the next page of results.
667
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400668Args:
669 previous_request: The request for the previous page. (required)
670 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400671
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400672Returns:
673 A request object that you can call 'execute()' on to request the next
674 page. Returns None if there are no more items in the collection.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400675 """
676 # Retrieve nextPageToken from previous_response
677 # Use as pageToken in previous_request to create new request.
678
679 if 'nextPageToken' not in previous_response:
680 return None
681
682 request = copy.copy(previous_request)
683
684 pageToken = previous_response['nextPageToken']
685 parsed = list(urlparse.urlparse(request.uri))
686 q = parse_qsl(parsed[4])
687
688 # Find and remove old 'pageToken' value from URI
689 newq = [(key, value) for (key, value) in q if key != 'pageToken']
690 newq.append(('pageToken', pageToken))
691 parsed[4] = urllib.urlencode(newq)
692 uri = urlparse.urlunparse(parsed)
693
694 request.uri = uri
695
Joe Gregorioe84c9442012-03-12 08:45:57 -0400696 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400697
698 return request
699
700 setattr(theclass, methodName, methodNext)
701
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400702 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400703 if 'methods' in resourceDesc:
704 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400705 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio708388c2012-06-15 13:43:04 -0400706 # Add in _media methods. The functionality of the attached method will
707 # change when it sees that the method name ends in _media.
708 if methodDesc.get('supportsMediaDownload', False):
709 createMethod(Resource, methodName + '_media', methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400710
711 # Add in nested resources
712 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500713
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400714 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
715 """Create a method on the Resource to access a nested Resource.
716
717 Args:
718 theclass: type, the class to attach methods to.
719 methodName: string, name of the method to use.
720 methodDesc: object, fragment of deserialized discovery document that
721 describes the method.
722 rootDesc: object, the entire deserialized discovery document.
723 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400724 methodName = fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400725
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500726 def methodResource(self):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400727 return _createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500728 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400729 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400730
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500731 setattr(methodResource, '__doc__', 'A collection resource.')
732 setattr(methodResource, '__is_resource__', True)
733 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400734
735 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400736 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400737
Joe Gregorio3c676f92011-07-25 10:38:14 -0400738 # Add _next() methods
739 # Look for response bodies in schema that contain nextPageToken, and methods
740 # that take a pageToken parameter.
741 if 'methods' in resourceDesc:
742 for methodName, methodDesc in resourceDesc['methods'].iteritems():
743 if 'response' in methodDesc:
744 responseSchema = methodDesc['response']
745 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500746 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400747 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
748 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400749 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
750 if hasNextPageToken and hasPageToken:
751 createNextMethod(Resource, methodName + '_next',
752 resourceDesc['methods'][methodName],
753 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400754
755 return Resource()