blob: b142ff148c1e378c2bc4549a58621b6cd20827fe [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,
211 base,
212 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.
225 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500226 http: httplib2.Http, An instance of httplib2.Http or something that acts
227 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500228 developerKey: string, Key for controlling API usage, generated
229 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400230 model: Model class instance that serializes and de-serializes requests and
231 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500232 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500233
234 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400235 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500236 """
237
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400238 # future is no longer used.
239 future = {}
240
Joe Gregorio292b9b82011-01-12 11:36:11 -0500241 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400242 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500243 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400244
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500245 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500246 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500247 model = JsonModel('dataWrapper' in features)
Joe Gregorioebd0b842012-06-15 14:14:17 -0400248 resource = _createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400249 service, service, schema)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400250
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500251 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400252
253
Joe Gregorio61d7e962011-02-22 22:52:07 -0500254def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500255 """Convert value to a string based on JSON Schema type.
256
257 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
258 JSON Schema.
259
260 Args:
261 value: any, the value to convert
262 schema_type: string, the type that value should be interpreted as
263
264 Returns:
265 A string representation of 'value' based on the schema_type.
266 """
267 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500268 if type(value) == type('') or type(value) == type(u''):
269 return value
270 else:
271 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500272 elif schema_type == 'integer':
273 return str(int(value))
274 elif schema_type == 'number':
275 return str(float(value))
276 elif schema_type == 'boolean':
277 return str(bool(value)).lower()
278 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500279 if type(value) == type('') or type(value) == type(u''):
280 return value
281 else:
282 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500283
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400284
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400285MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400286 "KB": 2 ** 10,
287 "MB": 2 ** 20,
288 "GB": 2 ** 30,
289 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400290 }
291
Joe Gregorioa98733f2011-09-16 10:12:28 -0400292
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400293def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400294 """Convert a string media size, such as 10GB or 3TB into an integer.
295
296 Args:
297 maxSize: string, size as a string, such as 2MB or 7GB.
298
299 Returns:
300 The size as an integer value.
301 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400302 if len(maxSize) < 2:
303 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400304 units = maxSize[-2:].upper()
305 multiplier = MULTIPLIERS.get(units, 0)
306 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400307 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400308 else:
309 return int(maxSize)
310
Joe Gregoriobee86832011-02-22 10:00:19 -0500311
Joe Gregorioebd0b842012-06-15 14:14:17 -0400312def _createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400313 developerKey, resourceDesc, rootDesc, schema):
314 """Build a Resource from the API description.
315
316 Args:
317 http: httplib2.Http, Object to make http requests with.
318 baseUrl: string, base URL for the API. All requests are relative to this
319 URI.
320 model: apiclient.Model, converts to and from the wire format.
321 requestBuilder: class or callable that instantiates an
322 apiclient.HttpRequest object.
323 developerKey: string, key obtained from
324 https://code.google.com/apis/console
325 resourceDesc: object, section of deserialized discovery document that
326 describes a resource. Note that the top level discovery document
327 is considered a resource.
328 rootDesc: object, the entire deserialized discovery document.
329 schema: object, mapping of schema names to schema descriptions.
330
331 Returns:
332 An instance of Resource with all the methods attached for interacting with
333 that resource.
334 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400335
336 class Resource(object):
337 """A class for interacting with a resource."""
338
339 def __init__(self):
340 self._http = http
341 self._baseUrl = baseUrl
342 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400343 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500344 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400345
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400346 def createMethod(theclass, methodName, methodDesc, rootDesc):
347 """Creates a method for attaching to a Resource.
348
349 Args:
350 theclass: type, the class to attach methods to.
351 methodName: string, name of the method to use.
352 methodDesc: object, fragment of deserialized discovery document that
353 describes the method.
354 rootDesc: object, the entire deserialized discovery document.
355 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400356 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400357 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400358 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400359 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400360
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400361 mediaPathUrl = None
362 accept = []
363 maxSize = 0
364 if 'mediaUpload' in methodDesc:
365 mediaUpload = methodDesc['mediaUpload']
Joe Gregoriode860442012-03-02 15:55:52 -0500366 # TODO(jcgregorio) Use URLs from discovery once it is updated.
367 parsed = list(urlparse.urlparse(baseUrl))
368 basePath = parsed[2]
369 mediaPathUrl = '/upload' + basePath + 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
509 # If there is no schema for the response then presume a binary blob.
Joe Gregorio708388c2012-06-15 13:43:04 -0400510 if methodName.endswith('_media'):
511 model = MediaModel()
512 elif 'response' not in methodDesc:
Joe Gregorioe08a1662011-12-07 09:48:22 -0500513 model = RawModel()
514
Joe Gregorio48d361f2010-08-18 13:19:21 -0400515 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500516 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400517 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400518
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400519 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400520 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
521
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500522 resumable = None
523 multipart_boundary = ''
524
Joe Gregorio922b78c2011-05-26 21:36:34 -0400525 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500526 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500527 if isinstance(media_filename, basestring):
528 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
529 if media_mime_type is None:
530 raise UnknownFileType(media_filename)
531 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
532 raise UnacceptableMimeTypeError(media_mime_type)
533 media_upload = MediaFileUpload(media_filename, media_mime_type)
534 elif isinstance(media_filename, MediaUpload):
535 media_upload = media_filename
536 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500537 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500538
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400539 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500540 if maxSize > 0 and media_upload.size() > maxSize:
541 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400542
543 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500544 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400545 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500546 if media_upload.resumable():
547 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400548
Joe Gregorio945be3e2012-01-27 17:01:06 -0500549 if media_upload.resumable():
550 # This is all we need to do for resumable, if the body exists it gets
551 # sent in the first request, otherwise an empty body is sent.
552 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400553 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500554 # A non-resumable upload
555 if body is None:
556 # This is a simple media upload
557 headers['content-type'] = media_upload.mimetype()
558 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500559 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500560 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500561 # This is a multipart/related upload.
562 msgRoot = MIMEMultipart('related')
563 # msgRoot should not write out it's own headers
564 setattr(msgRoot, '_write_headers', lambda self: None)
565
566 # attach the body as one part
567 msg = MIMENonMultipart(*headers['content-type'].split('/'))
568 msg.set_payload(body)
569 msgRoot.attach(msg)
570
571 # attach the media as the second part
572 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
573 msg['Content-Transfer-Encoding'] = 'binary'
574
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500575 payload = media_upload.getbytes(0, media_upload.size())
576 msg.set_payload(payload)
577 msgRoot.attach(msg)
578 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400579
Joe Gregorio945be3e2012-01-27 17:01:06 -0500580 multipart_boundary = msgRoot.get_boundary()
581 headers['content-type'] = ('multipart/related; '
582 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500583 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400584
Joe Gregorioe84c9442012-03-12 08:45:57 -0400585 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500586 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500587 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500588 url,
589 method=httpMethod,
590 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500591 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500592 methodId=methodId,
593 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400594
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500595 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
596 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500597 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400598
599 # Skip undocumented params and params common to all methods.
600 skip_parameters = rootDesc.get('parameters', {}).keys()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400601 skip_parameters.extend(STACK_QUERY_PARAMETERS)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400602
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400603 all_args = argmap.keys()
604 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
605
606 # Move body to the front of the line.
607 if 'body' in all_args:
608 args_ordered.append('body')
609
610 for name in all_args:
611 if name not in args_ordered:
612 args_ordered.append(name)
613
614 for arg in args_ordered:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400615 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500616 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400617
Joe Gregorio61d7e962011-02-22 22:52:07 -0500618 repeated = ''
619 if arg in repeated_params:
620 repeated = ' (repeated)'
621 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400622 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500623 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500624 paramdesc = methodDesc['parameters'][argmap[arg]]
625 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500626 if '$ref' in paramdesc:
627 docs.append(
628 (' %s: object, %s%s%s\n The object takes the'
629 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
630 schema.prettyPrintByName(paramdesc['$ref'])))
631 else:
632 paramtype = paramdesc.get('type', 'string')
633 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
634 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500635 enum = paramdesc.get('enum', [])
636 enumDesc = paramdesc.get('enumDescriptions', [])
637 if enum and enumDesc:
638 docs.append(' Allowed values\n')
639 for (name, desc) in zip(enum, enumDesc):
640 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500641 if 'response' in methodDesc:
Joe Gregorio708388c2012-06-15 13:43:04 -0400642 if methodName.endswith('_media'):
643 docs.append('\nReturns:\n The media object as a string.\n\n ')
644 else:
645 docs.append('\nReturns:\n An object of the form:\n\n ')
646 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400647
648 setattr(method, '__doc__', ''.join(docs))
649 setattr(theclass, methodName, method)
650
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400651 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
652 """Creates any _next methods for attaching to a Resource.
653
654 The _next methods allow for easy iteration through list() responses.
655
656 Args:
657 theclass: type, the class to attach methods to.
658 methodName: string, name of the method to use.
659 methodDesc: object, fragment of deserialized discovery document that
660 describes the method.
661 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400662 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400663 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400664 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400665
Joe Gregorio3c676f92011-07-25 10:38:14 -0400666 def methodNext(self, previous_request, previous_response):
667 """Retrieves the next page of results.
668
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400669Args:
670 previous_request: The request for the previous page. (required)
671 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400672
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400673Returns:
674 A request object that you can call 'execute()' on to request the next
675 page. Returns None if there are no more items in the collection.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400676 """
677 # Retrieve nextPageToken from previous_response
678 # Use as pageToken in previous_request to create new request.
679
680 if 'nextPageToken' not in previous_response:
681 return None
682
683 request = copy.copy(previous_request)
684
685 pageToken = previous_response['nextPageToken']
686 parsed = list(urlparse.urlparse(request.uri))
687 q = parse_qsl(parsed[4])
688
689 # Find and remove old 'pageToken' value from URI
690 newq = [(key, value) for (key, value) in q if key != 'pageToken']
691 newq.append(('pageToken', pageToken))
692 parsed[4] = urllib.urlencode(newq)
693 uri = urlparse.urlunparse(parsed)
694
695 request.uri = uri
696
Joe Gregorioe84c9442012-03-12 08:45:57 -0400697 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400698
699 return request
700
701 setattr(theclass, methodName, methodNext)
702
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400703 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400704 if 'methods' in resourceDesc:
705 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400706 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio708388c2012-06-15 13:43:04 -0400707 # Add in _media methods. The functionality of the attached method will
708 # change when it sees that the method name ends in _media.
709 if methodDesc.get('supportsMediaDownload', False):
710 createMethod(Resource, methodName + '_media', methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400711
712 # Add in nested resources
713 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500714
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400715 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
716 """Create a method on the Resource to access a nested Resource.
717
718 Args:
719 theclass: type, the class to attach methods to.
720 methodName: string, name of the method to use.
721 methodDesc: object, fragment of deserialized discovery document that
722 describes the method.
723 rootDesc: object, the entire deserialized discovery document.
724 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400725 methodName = fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400726
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500727 def methodResource(self):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400728 return _createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500729 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400730 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400731
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500732 setattr(methodResource, '__doc__', 'A collection resource.')
733 setattr(methodResource, '__is_resource__', True)
734 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400735
736 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400737 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400738
Joe Gregorio3c676f92011-07-25 10:38:14 -0400739 # Add _next() methods
740 # Look for response bodies in schema that contain nextPageToken, and methods
741 # that take a pageToken parameter.
742 if 'methods' in resourceDesc:
743 for methodName, methodDesc in resourceDesc['methods'].iteritems():
744 if 'response' in methodDesc:
745 responseSchema = methodDesc['response']
746 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500747 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400748 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
749 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400750 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
751 if hasNextPageToken and hasPageToken:
752 createNextMethod(Resource, methodName + '_next',
753 resourceDesc['methods'][methodName],
754 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400755
756 return Resource()