blob: 3e1964c47e8bfa7066fcdbd56a2feaad6b7e832c [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
55from apiclient.model import RawModel
56from apiclient.schema import Schemas
Joe Gregorio922b78c2011-05-26 21:36:34 -040057from email.mime.multipart import MIMEMultipart
58from email.mime.nonmultipart import MIMENonMultipart
Joe Gregorio549230c2012-01-11 10:38:05 -050059from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050060
Joe Gregorioe84c9442012-03-12 08:45:57 -040061logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040062
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050063URITEMPLATE = re.compile('{[^}]*}')
64VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040065DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
66 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050067DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050068
Joe Gregorioc8e421c2012-06-06 14:03:13 -040069# Parameters accepted by the stack, but not visible via discovery.
70STACK_QUERY_PARAMETERS = ['trace', 'pp', 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040071
Joe Gregorioc8e421c2012-06-06 14:03:13 -040072# Python reserved words.
Joe Gregorio562b7312011-09-15 09:06:38 -040073RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040074 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
75 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
76 'pass', 'print', 'raise', 'return', 'try', 'while' ]
77
Joe Gregorio562b7312011-09-15 09:06:38 -040078
Joe Gregorioce31a972012-06-06 15:48:17 -040079def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040080 """Fix method names to avoid reserved word conflicts.
81
82 Args:
83 name: string, method name.
84
85 Returns:
86 The name with a '_' prefixed if the name is a reserved word.
87 """
Joe Gregoriod92897c2011-07-07 11:44:56 -040088 if name in RESERVED_WORDS:
89 return name + '_'
90 else:
91 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040092
Joe Gregorioa98733f2011-09-16 10:12:28 -040093
Joe Gregorioa98733f2011-09-16 10:12:28 -040094def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050095 """Adds a query parameter to a url.
96
97 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -040098
99 Args:
100 url: string, url to add the query parameter to.
101 name: string, query parameter name.
102 value: string, query parameter value.
103
104 Returns:
105 Updated query parameter. Does not update the url if value is None.
106 """
107 if value is None:
108 return url
109 else:
110 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500111 q = dict(parse_qsl(parsed[4]))
112 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400113 parsed[4] = urllib.urlencode(q)
114 return urlparse.urlunparse(parsed)
115
116
Joe Gregorio48d361f2010-08-18 13:19:21 -0400117def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500118 """Converts key names into parameter names.
119
120 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400121
122 Args:
123 key: string, the method key name.
124
125 Returns:
126 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400127 """
128 result = []
129 key = list(key)
130 if not key[0].isalpha():
131 result.append('x')
132 for c in key:
133 if c.isalnum():
134 result.append(c)
135 else:
136 result.append('_')
137
138 return ''.join(result)
139
140
Joe Gregorio01770a52012-02-24 11:11:10 -0500141def build(serviceName,
142 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500143 http=None,
144 discoveryServiceUrl=DISCOVERY_URI,
145 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500146 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500147 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500148 """Construct a Resource for interacting with an API.
149
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400150 Construct a Resource object for interacting with an API. The serviceName and
151 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500152
153 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400154 serviceName: string, name of the service.
155 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500156 http: httplib2.Http, An instance of httplib2.Http or something that acts
157 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400158 discoveryServiceUrl: string, a URI Template that points to the location of
159 the discovery service. It should have two parameters {api} and
160 {apiVersion} that when filled in produce an absolute URI to the discovery
161 document for that service.
162 developerKey: string, key obtained from
163 https://code.google.com/apis/console.
164 model: apiclient.Model, converts to and from the wire format.
165 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
166 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500167
168 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400169 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500170 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400171 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400172 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400173 'apiVersion': version
174 }
ade@google.com850cf552010-08-20 23:24:56 +0100175
Joe Gregorioc204b642010-09-21 12:01:23 -0400176 if http is None:
177 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400178
ade@google.com850cf552010-08-20 23:24:56 +0100179 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400180
Joe Gregorio66f57522011-11-30 11:00:00 -0500181 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
182 # variable that contains the network address of the client sending the
183 # request. If it exists then add that to the request for the discovery
184 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400185 if 'REMOTE_ADDR' in os.environ:
186 requested_url = _add_query_parameter(requested_url, 'userIp',
187 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400188 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400189
ade@google.com850cf552010-08-20 23:24:56 +0100190 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400191
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500192 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500193 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500194 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400195 if resp.status >= 400:
Joe Gregorio49396552011-03-08 10:39:00 -0500196 raise HttpError(resp, content, requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400197
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500198 try:
199 service = simplejson.loads(content)
200 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400201 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500202 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400203
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400204 return build_from_document(content, discoveryServiceUrl, http=http,
205 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500206
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500207
Joe Gregorio292b9b82011-01-12 11:36:11 -0500208def build_from_document(
209 service,
210 base,
211 future=None,
212 http=None,
213 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500214 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500215 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500216 """Create a Resource for interacting with an API.
217
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400218 Same as `build()`, but constructs the Resource object from a discovery
219 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500220
Joe Gregorio292b9b82011-01-12 11:36:11 -0500221 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400222 service: string, discovery document.
223 base: string, base URI for all HTTP requests, usually the discovery URI.
224 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500225 http: httplib2.Http, An instance of httplib2.Http or something that acts
226 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500227 developerKey: string, Key for controlling API usage, generated
228 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400229 model: Model class instance that serializes and de-serializes requests and
230 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500231 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500232
233 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400234 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500235 """
236
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400237 # future is no longer used.
238 future = {}
239
Joe Gregorio292b9b82011-01-12 11:36:11 -0500240 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400241 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500242 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400243
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500244 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500245 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500246 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500247 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400248 service, service, schema)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400249
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500250 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400251
252
Joe Gregorio61d7e962011-02-22 22:52:07 -0500253def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500254 """Convert value to a string based on JSON Schema type.
255
256 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
257 JSON Schema.
258
259 Args:
260 value: any, the value to convert
261 schema_type: string, the type that value should be interpreted as
262
263 Returns:
264 A string representation of 'value' based on the schema_type.
265 """
266 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500267 if type(value) == type('') or type(value) == type(u''):
268 return value
269 else:
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400270 if value is None:
271 raise ValueError('String parameters can not be None.')
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500272 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500273 elif schema_type == 'integer':
274 return str(int(value))
275 elif schema_type == 'number':
276 return str(float(value))
277 elif schema_type == 'boolean':
278 return str(bool(value)).lower()
279 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500280 if type(value) == type('') or type(value) == type(u''):
281 return value
282 else:
283 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500284
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400285
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400286MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400287 "KB": 2 ** 10,
288 "MB": 2 ** 20,
289 "GB": 2 ** 30,
290 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400291 }
292
Joe Gregorioa98733f2011-09-16 10:12:28 -0400293
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400294def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400295 """Convert a string media size, such as 10GB or 3TB into an integer.
296
297 Args:
298 maxSize: string, size as a string, such as 2MB or 7GB.
299
300 Returns:
301 The size as an integer value.
302 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400303 if len(maxSize) < 2:
304 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400305 units = maxSize[-2:].upper()
306 multiplier = MULTIPLIERS.get(units, 0)
307 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400308 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400309 else:
310 return int(maxSize)
311
Joe Gregoriobee86832011-02-22 10:00:19 -0500312
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500313def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400314 developerKey, resourceDesc, rootDesc, schema):
315 """Build a Resource from the API description.
316
317 Args:
318 http: httplib2.Http, Object to make http requests with.
319 baseUrl: string, base URL for the API. All requests are relative to this
320 URI.
321 model: apiclient.Model, converts to and from the wire format.
322 requestBuilder: class or callable that instantiates an
323 apiclient.HttpRequest object.
324 developerKey: string, key obtained from
325 https://code.google.com/apis/console
326 resourceDesc: object, section of deserialized discovery document that
327 describes a resource. Note that the top level discovery document
328 is considered a resource.
329 rootDesc: object, the entire deserialized discovery document.
330 schema: object, mapping of schema names to schema descriptions.
331
332 Returns:
333 An instance of Resource with all the methods attached for interacting with
334 that resource.
335 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400336
337 class Resource(object):
338 """A class for interacting with a resource."""
339
340 def __init__(self):
341 self._http = http
342 self._baseUrl = baseUrl
343 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400344 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500345 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400346
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400347 def createMethod(theclass, methodName, methodDesc, rootDesc):
348 """Creates a method for attaching to a Resource.
349
350 Args:
351 theclass: type, the class to attach methods to.
352 methodName: string, name of the method to use.
353 methodDesc: object, fragment of deserialized discovery document that
354 describes the method.
355 rootDesc: object, the entire deserialized discovery document.
356 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400357 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400358 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400359 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400360 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400361
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400362 mediaPathUrl = None
363 accept = []
364 maxSize = 0
365 if 'mediaUpload' in methodDesc:
366 mediaUpload = methodDesc['mediaUpload']
Joe Gregoriode860442012-03-02 15:55:52 -0500367 # TODO(jcgregorio) Use URLs from discovery once it is updated.
368 parsed = list(urlparse.urlparse(baseUrl))
369 basePath = parsed[2]
370 mediaPathUrl = '/upload' + basePath + pathUrl
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400371 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400372 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400373
Joe Gregorioca876e42011-02-22 19:39:42 -0500374 if 'parameters' not in methodDesc:
375 methodDesc['parameters'] = {}
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400376
377 # Add in the parameters common to all methods.
378 for name, desc in rootDesc.get('parameters', {}).iteritems():
379 methodDesc['parameters'][name] = desc
380
381 # Add in undocumented query parameters.
Joe Gregorioca876e42011-02-22 19:39:42 -0500382 for name in STACK_QUERY_PARAMETERS:
383 methodDesc['parameters'][name] = {
384 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400385 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500386 }
387
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500388 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500389 methodDesc['parameters']['body'] = {
390 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500391 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500392 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500393 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500394 if 'request' in methodDesc:
395 methodDesc['parameters']['body'].update(methodDesc['request'])
396 else:
397 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500398 if 'mediaUpload' in methodDesc:
399 methodDesc['parameters']['media_body'] = {
400 'description': 'The filename of the media request body.',
401 'type': 'string',
402 'required': False,
403 }
404 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400405 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100406
Joe Gregorioca876e42011-02-22 19:39:42 -0500407 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100408 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500409 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100410 pattern_params = {} # Parameters that must match a regex
411 query_params = [] # Parameters that will be used in the query string
412 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500413 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500414 enum_params = {} # Allowable enumeration values for each parameter
415
416
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400417 if 'parameters' in methodDesc:
418 for arg, desc in methodDesc['parameters'].iteritems():
419 param = key2param(arg)
420 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400421
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400422 if desc.get('pattern', ''):
423 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500424 if desc.get('enum', ''):
425 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400426 if desc.get('required', False):
427 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500428 if desc.get('repeated', False):
429 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400430 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400431 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400432 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400433 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500434 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400435
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500436 for match in URITEMPLATE.finditer(pathUrl):
437 for namematch in VARNAME.finditer(match.group(0)):
438 name = key2param(namematch.group(0))
439 path_params[name] = name
440 if name in query_params:
441 query_params.remove(name)
442
Joe Gregorio48d361f2010-08-18 13:19:21 -0400443 def method(self, **kwargs):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400444 # Don't bother with doc string, it will be over-written by createMethod.
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
ade@google.com850cf552010-08-20 23:24:56 +0100449 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400450 if name not in kwargs:
451 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400452
ade@google.com850cf552010-08-20 23:24:56 +0100453 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400454 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500455 if isinstance(kwargs[name], basestring):
456 pvalues = [kwargs[name]]
457 else:
458 pvalues = kwargs[name]
459 for pvalue in pvalues:
460 if re.match(regex, pvalue) is None:
461 raise TypeError(
462 'Parameter "%s" value "%s" does not match the pattern "%s"' %
463 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400464
Joe Gregoriobee86832011-02-22 10:00:19 -0500465 for name, enums in enum_params.iteritems():
466 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800467 # We need to handle the case of a repeated enum
468 # name differently, since we want to handle both
469 # arg='value' and arg=['value1', 'value2']
470 if (name in repeated_params and
471 not isinstance(kwargs[name], basestring)):
472 values = kwargs[name]
473 else:
474 values = [kwargs[name]]
475 for value in values:
476 if value not in enums:
477 raise TypeError(
478 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
479 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500480
ade@google.com850cf552010-08-20 23:24:56 +0100481 actual_query_params = {}
482 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400483 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500484 to_type = param_type.get(key, 'string')
485 # For repeated parameters we cast each member of the list.
486 if key in repeated_params and type(value) == type([]):
487 cast_value = [_cast(x, to_type) for x in value]
488 else:
489 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100490 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500491 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100492 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500493 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100494 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400495 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400496
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400497 if self._developerKey:
498 actual_query_params['key'] = self._developerKey
499
Joe Gregorioe08a1662011-12-07 09:48:22 -0500500 model = self._model
501 # If there is no schema for the response then presume a binary blob.
502 if 'response' not in methodDesc:
503 model = RawModel()
504
Joe Gregorio48d361f2010-08-18 13:19:21 -0400505 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500506 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400507 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400508
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400509 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400510 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
511
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500512 resumable = None
513 multipart_boundary = ''
514
Joe Gregorio922b78c2011-05-26 21:36:34 -0400515 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500516 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500517 if isinstance(media_filename, basestring):
518 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
519 if media_mime_type is None:
520 raise UnknownFileType(media_filename)
521 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
522 raise UnacceptableMimeTypeError(media_mime_type)
523 media_upload = MediaFileUpload(media_filename, media_mime_type)
524 elif isinstance(media_filename, MediaUpload):
525 media_upload = media_filename
526 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500527 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500528
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400529 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500530 if maxSize > 0 and media_upload.size() > maxSize:
531 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400532
533 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500534 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400535 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500536 if media_upload.resumable():
537 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400538
Joe Gregorio945be3e2012-01-27 17:01:06 -0500539 if media_upload.resumable():
540 # This is all we need to do for resumable, if the body exists it gets
541 # sent in the first request, otherwise an empty body is sent.
542 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400543 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500544 # A non-resumable upload
545 if body is None:
546 # This is a simple media upload
547 headers['content-type'] = media_upload.mimetype()
548 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500549 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500550 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500551 # This is a multipart/related upload.
552 msgRoot = MIMEMultipart('related')
553 # msgRoot should not write out it's own headers
554 setattr(msgRoot, '_write_headers', lambda self: None)
555
556 # attach the body as one part
557 msg = MIMENonMultipart(*headers['content-type'].split('/'))
558 msg.set_payload(body)
559 msgRoot.attach(msg)
560
561 # attach the media as the second part
562 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
563 msg['Content-Transfer-Encoding'] = 'binary'
564
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500565 payload = media_upload.getbytes(0, media_upload.size())
566 msg.set_payload(payload)
567 msgRoot.attach(msg)
568 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400569
Joe Gregorio945be3e2012-01-27 17:01:06 -0500570 multipart_boundary = msgRoot.get_boundary()
571 headers['content-type'] = ('multipart/related; '
572 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500573 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400574
Joe Gregorioe84c9442012-03-12 08:45:57 -0400575 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500576 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500577 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500578 url,
579 method=httpMethod,
580 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500581 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500582 methodId=methodId,
583 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400584
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500585 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
586 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500587 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400588
589 # Skip undocumented params and params common to all methods.
590 skip_parameters = rootDesc.get('parameters', {}).keys()
591 skip_parameters.append(STACK_QUERY_PARAMETERS)
592
Joe Gregorio48d361f2010-08-18 13:19:21 -0400593 for arg in argmap.iterkeys():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400594 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500595 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400596
Joe Gregorio61d7e962011-02-22 22:52:07 -0500597 repeated = ''
598 if arg in repeated_params:
599 repeated = ' (repeated)'
600 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400601 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500602 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500603 paramdesc = methodDesc['parameters'][argmap[arg]]
604 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500605 if '$ref' in paramdesc:
606 docs.append(
607 (' %s: object, %s%s%s\n The object takes the'
608 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
609 schema.prettyPrintByName(paramdesc['$ref'])))
610 else:
611 paramtype = paramdesc.get('type', 'string')
612 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
613 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500614 enum = paramdesc.get('enum', [])
615 enumDesc = paramdesc.get('enumDescriptions', [])
616 if enum and enumDesc:
617 docs.append(' Allowed values\n')
618 for (name, desc) in zip(enum, enumDesc):
619 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500620 if 'response' in methodDesc:
621 docs.append('\nReturns:\n An object of the form\n\n ')
622 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400623
624 setattr(method, '__doc__', ''.join(docs))
625 setattr(theclass, methodName, method)
626
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400627 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
628 """Creates any _next methods for attaching to a Resource.
629
630 The _next methods allow for easy iteration through list() responses.
631
632 Args:
633 theclass: type, the class to attach methods to.
634 methodName: string, name of the method to use.
635 methodDesc: object, fragment of deserialized discovery document that
636 describes the method.
637 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400638 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400639 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400640 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400641
Joe Gregorio3c676f92011-07-25 10:38:14 -0400642 def methodNext(self, previous_request, previous_response):
643 """Retrieves the next page of results.
644
645 Args:
646 previous_request: The request for the previous page.
647 previous_response: The response from the request for the previous page.
648
649 Returns:
650 A request object that you can call 'execute()' on to request the next
651 page. Returns None if there are no more items in the collection.
652 """
653 # Retrieve nextPageToken from previous_response
654 # Use as pageToken in previous_request to create new request.
655
656 if 'nextPageToken' not in previous_response:
657 return None
658
659 request = copy.copy(previous_request)
660
661 pageToken = previous_response['nextPageToken']
662 parsed = list(urlparse.urlparse(request.uri))
663 q = parse_qsl(parsed[4])
664
665 # Find and remove old 'pageToken' value from URI
666 newq = [(key, value) for (key, value) in q if key != 'pageToken']
667 newq.append(('pageToken', pageToken))
668 parsed[4] = urllib.urlencode(newq)
669 uri = urlparse.urlunparse(parsed)
670
671 request.uri = uri
672
Joe Gregorioe84c9442012-03-12 08:45:57 -0400673 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400674
675 return request
676
677 setattr(theclass, methodName, methodNext)
678
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400679 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400680 if 'methods' in resourceDesc:
681 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400682 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400683
684 # Add in nested resources
685 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500686
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400687 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
688 """Create a method on the Resource to access a nested Resource.
689
690 Args:
691 theclass: type, the class to attach methods to.
692 methodName: string, name of the method to use.
693 methodDesc: object, fragment of deserialized discovery document that
694 describes the method.
695 rootDesc: object, the entire deserialized discovery document.
696 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400697 methodName = fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400698
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500699 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400700 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500701 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400702 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400703
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500704 setattr(methodResource, '__doc__', 'A collection resource.')
705 setattr(methodResource, '__is_resource__', True)
706 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400707
708 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400709 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400710
Joe Gregorio3c676f92011-07-25 10:38:14 -0400711 # Add _next() methods
712 # Look for response bodies in schema that contain nextPageToken, and methods
713 # that take a pageToken parameter.
714 if 'methods' in resourceDesc:
715 for methodName, methodDesc in resourceDesc['methods'].iteritems():
716 if 'response' in methodDesc:
717 responseSchema = methodDesc['response']
718 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500719 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400720 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
721 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400722 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
723 if hasNextPageToken and hasPageToken:
724 createNextMethod(Resource, methodName + '_next',
725 resourceDesc['methods'][methodName],
726 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400727
728 return Resource()