blob: ce54f883b54789b87dda9320cde60f047aac28bc [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',
77 'pass', 'print', 'raise', 'return', 'try', 'while' ]
78
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'] = {
399 'description': 'The filename of the media request body.',
400 'type': 'string',
401 'required': False,
402 }
403 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400404 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100405
Joe Gregorioca876e42011-02-22 19:39:42 -0500406 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100407 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500408 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100409 pattern_params = {} # Parameters that must match a regex
410 query_params = [] # Parameters that will be used in the query string
411 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500412 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500413 enum_params = {} # Allowable enumeration values for each parameter
414
415
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400416 if 'parameters' in methodDesc:
417 for arg, desc in methodDesc['parameters'].iteritems():
418 param = key2param(arg)
419 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400420
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400421 if desc.get('pattern', ''):
422 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500423 if desc.get('enum', ''):
424 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400425 if desc.get('required', False):
426 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500427 if desc.get('repeated', False):
428 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400429 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400430 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400431 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400432 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500433 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400434
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500435 for match in URITEMPLATE.finditer(pathUrl):
436 for namematch in VARNAME.finditer(match.group(0)):
437 name = key2param(namematch.group(0))
438 path_params[name] = name
439 if name in query_params:
440 query_params.remove(name)
441
Joe Gregorio48d361f2010-08-18 13:19:21 -0400442 def method(self, **kwargs):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400443 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorio2467afa2012-06-20 12:21:25 -0400444
Joe Gregorio48d361f2010-08-18 13:19:21 -0400445 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500446 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400447 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400448
Joe Gregorio2467afa2012-06-20 12:21:25 -0400449 # Remove args that have a value of None.
450 keys = kwargs.keys()
451 for name in keys:
452 if kwargs[name] is None:
453 del kwargs[name]
454
ade@google.com850cf552010-08-20 23:24:56 +0100455 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400456 if name not in kwargs:
457 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400458
ade@google.com850cf552010-08-20 23:24:56 +0100459 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400460 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500461 if isinstance(kwargs[name], basestring):
462 pvalues = [kwargs[name]]
463 else:
464 pvalues = kwargs[name]
465 for pvalue in pvalues:
466 if re.match(regex, pvalue) is None:
467 raise TypeError(
468 'Parameter "%s" value "%s" does not match the pattern "%s"' %
469 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400470
Joe Gregoriobee86832011-02-22 10:00:19 -0500471 for name, enums in enum_params.iteritems():
472 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800473 # We need to handle the case of a repeated enum
474 # name differently, since we want to handle both
475 # arg='value' and arg=['value1', 'value2']
476 if (name in repeated_params and
477 not isinstance(kwargs[name], basestring)):
478 values = kwargs[name]
479 else:
480 values = [kwargs[name]]
481 for value in values:
482 if value not in enums:
483 raise TypeError(
484 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
485 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500486
ade@google.com850cf552010-08-20 23:24:56 +0100487 actual_query_params = {}
488 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400489 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500490 to_type = param_type.get(key, 'string')
491 # For repeated parameters we cast each member of the list.
492 if key in repeated_params and type(value) == type([]):
493 cast_value = [_cast(x, to_type) for x in value]
494 else:
495 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100496 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500497 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100498 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500499 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100500 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400501 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400502
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400503 if self._developerKey:
504 actual_query_params['key'] = self._developerKey
505
Joe Gregorioe08a1662011-12-07 09:48:22 -0500506 model = self._model
507 # If there is no schema for the response then presume a binary blob.
Joe Gregorio708388c2012-06-15 13:43:04 -0400508 if methodName.endswith('_media'):
509 model = MediaModel()
510 elif 'response' not in methodDesc:
Joe Gregorioe08a1662011-12-07 09:48:22 -0500511 model = RawModel()
512
Joe Gregorio48d361f2010-08-18 13:19:21 -0400513 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500514 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400515 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400516
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400517 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400518 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
519
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500520 resumable = None
521 multipart_boundary = ''
522
Joe Gregorio922b78c2011-05-26 21:36:34 -0400523 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500524 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500525 if isinstance(media_filename, basestring):
526 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
527 if media_mime_type is None:
528 raise UnknownFileType(media_filename)
529 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
530 raise UnacceptableMimeTypeError(media_mime_type)
531 media_upload = MediaFileUpload(media_filename, media_mime_type)
532 elif isinstance(media_filename, MediaUpload):
533 media_upload = media_filename
534 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500535 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500536
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400537 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500538 if maxSize > 0 and media_upload.size() > maxSize:
539 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400540
541 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500542 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400543 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500544 if media_upload.resumable():
545 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400546
Joe Gregorio945be3e2012-01-27 17:01:06 -0500547 if media_upload.resumable():
548 # This is all we need to do for resumable, if the body exists it gets
549 # sent in the first request, otherwise an empty body is sent.
550 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400551 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500552 # A non-resumable upload
553 if body is None:
554 # This is a simple media upload
555 headers['content-type'] = media_upload.mimetype()
556 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500557 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500558 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500559 # This is a multipart/related upload.
560 msgRoot = MIMEMultipart('related')
561 # msgRoot should not write out it's own headers
562 setattr(msgRoot, '_write_headers', lambda self: None)
563
564 # attach the body as one part
565 msg = MIMENonMultipart(*headers['content-type'].split('/'))
566 msg.set_payload(body)
567 msgRoot.attach(msg)
568
569 # attach the media as the second part
570 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
571 msg['Content-Transfer-Encoding'] = 'binary'
572
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500573 payload = media_upload.getbytes(0, media_upload.size())
574 msg.set_payload(payload)
575 msgRoot.attach(msg)
576 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400577
Joe Gregorio945be3e2012-01-27 17:01:06 -0500578 multipart_boundary = msgRoot.get_boundary()
579 headers['content-type'] = ('multipart/related; '
580 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500581 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400582
Joe Gregorioe84c9442012-03-12 08:45:57 -0400583 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500584 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500585 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500586 url,
587 method=httpMethod,
588 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500589 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500590 methodId=methodId,
591 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400592
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500593 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
594 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500595 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400596
597 # Skip undocumented params and params common to all methods.
598 skip_parameters = rootDesc.get('parameters', {}).keys()
599 skip_parameters.append(STACK_QUERY_PARAMETERS)
600
Joe Gregorio48d361f2010-08-18 13:19:21 -0400601 for arg in argmap.iterkeys():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400602 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500603 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400604
Joe Gregorio61d7e962011-02-22 22:52:07 -0500605 repeated = ''
606 if arg in repeated_params:
607 repeated = ' (repeated)'
608 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400609 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500610 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500611 paramdesc = methodDesc['parameters'][argmap[arg]]
612 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500613 if '$ref' in paramdesc:
614 docs.append(
615 (' %s: object, %s%s%s\n The object takes the'
616 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
617 schema.prettyPrintByName(paramdesc['$ref'])))
618 else:
619 paramtype = paramdesc.get('type', 'string')
620 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
621 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500622 enum = paramdesc.get('enum', [])
623 enumDesc = paramdesc.get('enumDescriptions', [])
624 if enum and enumDesc:
625 docs.append(' Allowed values\n')
626 for (name, desc) in zip(enum, enumDesc):
627 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500628 if 'response' in methodDesc:
Joe Gregorio708388c2012-06-15 13:43:04 -0400629 if methodName.endswith('_media'):
630 docs.append('\nReturns:\n The media object as a string.\n\n ')
631 else:
632 docs.append('\nReturns:\n An object of the form:\n\n ')
633 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400634
635 setattr(method, '__doc__', ''.join(docs))
636 setattr(theclass, methodName, method)
637
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400638 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
639 """Creates any _next methods for attaching to a Resource.
640
641 The _next methods allow for easy iteration through list() responses.
642
643 Args:
644 theclass: type, the class to attach methods to.
645 methodName: string, name of the method to use.
646 methodDesc: object, fragment of deserialized discovery document that
647 describes the method.
648 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400649 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400650 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400651 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400652
Joe Gregorio3c676f92011-07-25 10:38:14 -0400653 def methodNext(self, previous_request, previous_response):
654 """Retrieves the next page of results.
655
656 Args:
657 previous_request: The request for the previous page.
658 previous_response: The response from the request for the previous page.
659
660 Returns:
661 A request object that you can call 'execute()' on to request the next
662 page. Returns None if there are no more items in the collection.
663 """
664 # Retrieve nextPageToken from previous_response
665 # Use as pageToken in previous_request to create new request.
666
667 if 'nextPageToken' not in previous_response:
668 return None
669
670 request = copy.copy(previous_request)
671
672 pageToken = previous_response['nextPageToken']
673 parsed = list(urlparse.urlparse(request.uri))
674 q = parse_qsl(parsed[4])
675
676 # Find and remove old 'pageToken' value from URI
677 newq = [(key, value) for (key, value) in q if key != 'pageToken']
678 newq.append(('pageToken', pageToken))
679 parsed[4] = urllib.urlencode(newq)
680 uri = urlparse.urlunparse(parsed)
681
682 request.uri = uri
683
Joe Gregorioe84c9442012-03-12 08:45:57 -0400684 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400685
686 return request
687
688 setattr(theclass, methodName, methodNext)
689
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400690 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400691 if 'methods' in resourceDesc:
692 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400693 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio708388c2012-06-15 13:43:04 -0400694 # Add in _media methods. The functionality of the attached method will
695 # change when it sees that the method name ends in _media.
696 if methodDesc.get('supportsMediaDownload', False):
697 createMethod(Resource, methodName + '_media', methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400698
699 # Add in nested resources
700 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500701
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400702 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
703 """Create a method on the Resource to access a nested Resource.
704
705 Args:
706 theclass: type, the class to attach methods to.
707 methodName: string, name of the method to use.
708 methodDesc: object, fragment of deserialized discovery document that
709 describes the method.
710 rootDesc: object, the entire deserialized discovery document.
711 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400712 methodName = fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400713
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500714 def methodResource(self):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400715 return _createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500716 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400717 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400718
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500719 setattr(methodResource, '__doc__', 'A collection resource.')
720 setattr(methodResource, '__is_resource__', True)
721 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400722
723 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400724 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400725
Joe Gregorio3c676f92011-07-25 10:38:14 -0400726 # Add _next() methods
727 # Look for response bodies in schema that contain nextPageToken, and methods
728 # that take a pageToken parameter.
729 if 'methods' in resourceDesc:
730 for methodName, methodDesc in resourceDesc['methods'].iteritems():
731 if 'response' in methodDesc:
732 responseSchema = methodDesc['response']
733 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500734 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400735 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
736 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400737 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
738 if hasNextPageToken and hasPageToken:
739 createNextMethod(Resource, methodName + '_next',
740 resourceDesc['methods'][methodName],
741 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400742
743 return Resource()