blob: c54b57f80119c4cfb0b40c69d87eff272529dc01 [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:
270 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500271 elif schema_type == 'integer':
272 return str(int(value))
273 elif schema_type == 'number':
274 return str(float(value))
275 elif schema_type == 'boolean':
276 return str(bool(value)).lower()
277 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500278 if type(value) == type('') or type(value) == type(u''):
279 return value
280 else:
281 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500282
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400283
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400284MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400285 "KB": 2 ** 10,
286 "MB": 2 ** 20,
287 "GB": 2 ** 30,
288 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400289 }
290
Joe Gregorioa98733f2011-09-16 10:12:28 -0400291
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400292def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400293 """Convert a string media size, such as 10GB or 3TB into an integer.
294
295 Args:
296 maxSize: string, size as a string, such as 2MB or 7GB.
297
298 Returns:
299 The size as an integer value.
300 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400301 if len(maxSize) < 2:
302 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400303 units = maxSize[-2:].upper()
304 multiplier = MULTIPLIERS.get(units, 0)
305 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400306 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400307 else:
308 return int(maxSize)
309
Joe Gregoriobee86832011-02-22 10:00:19 -0500310
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500311def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400312 developerKey, resourceDesc, rootDesc, schema):
313 """Build a Resource from the API description.
314
315 Args:
316 http: httplib2.Http, Object to make http requests with.
317 baseUrl: string, base URL for the API. All requests are relative to this
318 URI.
319 model: apiclient.Model, converts to and from the wire format.
320 requestBuilder: class or callable that instantiates an
321 apiclient.HttpRequest object.
322 developerKey: string, key obtained from
323 https://code.google.com/apis/console
324 resourceDesc: object, section of deserialized discovery document that
325 describes a resource. Note that the top level discovery document
326 is considered a resource.
327 rootDesc: object, the entire deserialized discovery document.
328 schema: object, mapping of schema names to schema descriptions.
329
330 Returns:
331 An instance of Resource with all the methods attached for interacting with
332 that resource.
333 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400334
335 class Resource(object):
336 """A class for interacting with a resource."""
337
338 def __init__(self):
339 self._http = http
340 self._baseUrl = baseUrl
341 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400342 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500343 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400344
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400345 def createMethod(theclass, methodName, methodDesc, rootDesc):
346 """Creates a method for attaching to a Resource.
347
348 Args:
349 theclass: type, the class to attach methods to.
350 methodName: string, name of the method to use.
351 methodDesc: object, fragment of deserialized discovery document that
352 describes the method.
353 rootDesc: object, the entire deserialized discovery document.
354 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400355 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400356 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400357 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400358 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400359
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400360 mediaPathUrl = None
361 accept = []
362 maxSize = 0
363 if 'mediaUpload' in methodDesc:
364 mediaUpload = methodDesc['mediaUpload']
Joe Gregoriode860442012-03-02 15:55:52 -0500365 # TODO(jcgregorio) Use URLs from discovery once it is updated.
366 parsed = list(urlparse.urlparse(baseUrl))
367 basePath = parsed[2]
368 mediaPathUrl = '/upload' + basePath + pathUrl
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400369 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400370 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400371
Joe Gregorioca876e42011-02-22 19:39:42 -0500372 if 'parameters' not in methodDesc:
373 methodDesc['parameters'] = {}
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400374
375 # Add in the parameters common to all methods.
376 for name, desc in rootDesc.get('parameters', {}).iteritems():
377 methodDesc['parameters'][name] = desc
378
379 # Add in undocumented query parameters.
Joe Gregorioca876e42011-02-22 19:39:42 -0500380 for name in STACK_QUERY_PARAMETERS:
381 methodDesc['parameters'][name] = {
382 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400383 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500384 }
385
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500386 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500387 methodDesc['parameters']['body'] = {
388 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500389 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500390 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500391 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500392 if 'request' in methodDesc:
393 methodDesc['parameters']['body'].update(methodDesc['request'])
394 else:
395 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500396 if 'mediaUpload' in methodDesc:
397 methodDesc['parameters']['media_body'] = {
398 'description': 'The filename of the media request body.',
399 'type': 'string',
400 'required': False,
401 }
402 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400403 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100404
Joe Gregorioca876e42011-02-22 19:39:42 -0500405 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100406 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500407 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100408 pattern_params = {} # Parameters that must match a regex
409 query_params = [] # Parameters that will be used in the query string
410 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500411 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500412 enum_params = {} # Allowable enumeration values for each parameter
413
414
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400415 if 'parameters' in methodDesc:
416 for arg, desc in methodDesc['parameters'].iteritems():
417 param = key2param(arg)
418 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400419
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400420 if desc.get('pattern', ''):
421 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500422 if desc.get('enum', ''):
423 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400424 if desc.get('required', False):
425 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500426 if desc.get('repeated', False):
427 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400428 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400429 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400430 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400431 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500432 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400433
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500434 for match in URITEMPLATE.finditer(pathUrl):
435 for namematch in VARNAME.finditer(match.group(0)):
436 name = key2param(namematch.group(0))
437 path_params[name] = name
438 if name in query_params:
439 query_params.remove(name)
440
Joe Gregorio48d361f2010-08-18 13:19:21 -0400441 def method(self, **kwargs):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400442 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400443 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500444 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400445 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400446
ade@google.com850cf552010-08-20 23:24:56 +0100447 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400448 if name not in kwargs:
449 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400450
ade@google.com850cf552010-08-20 23:24:56 +0100451 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400452 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500453 if isinstance(kwargs[name], basestring):
454 pvalues = [kwargs[name]]
455 else:
456 pvalues = kwargs[name]
457 for pvalue in pvalues:
458 if re.match(regex, pvalue) is None:
459 raise TypeError(
460 'Parameter "%s" value "%s" does not match the pattern "%s"' %
461 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400462
Joe Gregoriobee86832011-02-22 10:00:19 -0500463 for name, enums in enum_params.iteritems():
464 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800465 # We need to handle the case of a repeated enum
466 # name differently, since we want to handle both
467 # arg='value' and arg=['value1', 'value2']
468 if (name in repeated_params and
469 not isinstance(kwargs[name], basestring)):
470 values = kwargs[name]
471 else:
472 values = [kwargs[name]]
473 for value in values:
474 if value not in enums:
475 raise TypeError(
476 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
477 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500478
ade@google.com850cf552010-08-20 23:24:56 +0100479 actual_query_params = {}
480 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400481 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500482 to_type = param_type.get(key, 'string')
483 # For repeated parameters we cast each member of the list.
484 if key in repeated_params and type(value) == type([]):
485 cast_value = [_cast(x, to_type) for x in value]
486 else:
487 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100488 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500489 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100490 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500491 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100492 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400493 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400494
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400495 if self._developerKey:
496 actual_query_params['key'] = self._developerKey
497
Joe Gregorioe08a1662011-12-07 09:48:22 -0500498 model = self._model
499 # If there is no schema for the response then presume a binary blob.
500 if 'response' not in methodDesc:
501 model = RawModel()
502
Joe Gregorio48d361f2010-08-18 13:19:21 -0400503 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500504 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400505 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400506
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400507 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400508 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
509
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500510 resumable = None
511 multipart_boundary = ''
512
Joe Gregorio922b78c2011-05-26 21:36:34 -0400513 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500514 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500515 if isinstance(media_filename, basestring):
516 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
517 if media_mime_type is None:
518 raise UnknownFileType(media_filename)
519 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
520 raise UnacceptableMimeTypeError(media_mime_type)
521 media_upload = MediaFileUpload(media_filename, media_mime_type)
522 elif isinstance(media_filename, MediaUpload):
523 media_upload = media_filename
524 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500525 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500526
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400527 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500528 if maxSize > 0 and media_upload.size() > maxSize:
529 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400530
531 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500532 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400533 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500534 if media_upload.resumable():
535 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400536
Joe Gregorio945be3e2012-01-27 17:01:06 -0500537 if media_upload.resumable():
538 # This is all we need to do for resumable, if the body exists it gets
539 # sent in the first request, otherwise an empty body is sent.
540 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400541 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500542 # A non-resumable upload
543 if body is None:
544 # This is a simple media upload
545 headers['content-type'] = media_upload.mimetype()
546 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500547 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500548 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500549 # This is a multipart/related upload.
550 msgRoot = MIMEMultipart('related')
551 # msgRoot should not write out it's own headers
552 setattr(msgRoot, '_write_headers', lambda self: None)
553
554 # attach the body as one part
555 msg = MIMENonMultipart(*headers['content-type'].split('/'))
556 msg.set_payload(body)
557 msgRoot.attach(msg)
558
559 # attach the media as the second part
560 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
561 msg['Content-Transfer-Encoding'] = 'binary'
562
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500563 payload = media_upload.getbytes(0, media_upload.size())
564 msg.set_payload(payload)
565 msgRoot.attach(msg)
566 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400567
Joe Gregorio945be3e2012-01-27 17:01:06 -0500568 multipart_boundary = msgRoot.get_boundary()
569 headers['content-type'] = ('multipart/related; '
570 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500571 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400572
Joe Gregorioe84c9442012-03-12 08:45:57 -0400573 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500574 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500575 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500576 url,
577 method=httpMethod,
578 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500579 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500580 methodId=methodId,
581 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400582
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500583 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
584 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500585 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400586
587 # Skip undocumented params and params common to all methods.
588 skip_parameters = rootDesc.get('parameters', {}).keys()
589 skip_parameters.append(STACK_QUERY_PARAMETERS)
590
Joe Gregorio48d361f2010-08-18 13:19:21 -0400591 for arg in argmap.iterkeys():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400592 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500593 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400594
Joe Gregorio61d7e962011-02-22 22:52:07 -0500595 repeated = ''
596 if arg in repeated_params:
597 repeated = ' (repeated)'
598 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400599 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500600 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500601 paramdesc = methodDesc['parameters'][argmap[arg]]
602 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500603 if '$ref' in paramdesc:
604 docs.append(
605 (' %s: object, %s%s%s\n The object takes the'
606 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
607 schema.prettyPrintByName(paramdesc['$ref'])))
608 else:
609 paramtype = paramdesc.get('type', 'string')
610 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
611 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500612 enum = paramdesc.get('enum', [])
613 enumDesc = paramdesc.get('enumDescriptions', [])
614 if enum and enumDesc:
615 docs.append(' Allowed values\n')
616 for (name, desc) in zip(enum, enumDesc):
617 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500618 if 'response' in methodDesc:
619 docs.append('\nReturns:\n An object of the form\n\n ')
620 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400621
622 setattr(method, '__doc__', ''.join(docs))
623 setattr(theclass, methodName, method)
624
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400625 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
626 """Creates any _next methods for attaching to a Resource.
627
628 The _next methods allow for easy iteration through list() responses.
629
630 Args:
631 theclass: type, the class to attach methods to.
632 methodName: string, name of the method to use.
633 methodDesc: object, fragment of deserialized discovery document that
634 describes the method.
635 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400636 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400637 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400638 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400639
Joe Gregorio3c676f92011-07-25 10:38:14 -0400640 def methodNext(self, previous_request, previous_response):
641 """Retrieves the next page of results.
642
643 Args:
644 previous_request: The request for the previous page.
645 previous_response: The response from the request for the previous page.
646
647 Returns:
648 A request object that you can call 'execute()' on to request the next
649 page. Returns None if there are no more items in the collection.
650 """
651 # Retrieve nextPageToken from previous_response
652 # Use as pageToken in previous_request to create new request.
653
654 if 'nextPageToken' not in previous_response:
655 return None
656
657 request = copy.copy(previous_request)
658
659 pageToken = previous_response['nextPageToken']
660 parsed = list(urlparse.urlparse(request.uri))
661 q = parse_qsl(parsed[4])
662
663 # Find and remove old 'pageToken' value from URI
664 newq = [(key, value) for (key, value) in q if key != 'pageToken']
665 newq.append(('pageToken', pageToken))
666 parsed[4] = urllib.urlencode(newq)
667 uri = urlparse.urlunparse(parsed)
668
669 request.uri = uri
670
Joe Gregorioe84c9442012-03-12 08:45:57 -0400671 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400672
673 return request
674
675 setattr(theclass, methodName, methodNext)
676
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400677 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400678 if 'methods' in resourceDesc:
679 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400680 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400681
682 # Add in nested resources
683 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500684
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400685 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
686 """Create a method on the Resource to access a nested Resource.
687
688 Args:
689 theclass: type, the class to attach methods to.
690 methodName: string, name of the method to use.
691 methodDesc: object, fragment of deserialized discovery document that
692 describes the method.
693 rootDesc: object, the entire deserialized discovery document.
694 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400695 methodName = fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400696
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500697 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400698 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500699 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400700 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400701
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500702 setattr(methodResource, '__doc__', 'A collection resource.')
703 setattr(methodResource, '__is_resource__', True)
704 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400705
706 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400707 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400708
Joe Gregorio3c676f92011-07-25 10:38:14 -0400709 # Add _next() methods
710 # Look for response bodies in schema that contain nextPageToken, and methods
711 # that take a pageToken parameter.
712 if 'methods' in resourceDesc:
713 for methodName, methodDesc in resourceDesc['methods'].iteritems():
714 if 'response' in methodDesc:
715 responseSchema = methodDesc['response']
716 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500717 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400718 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
719 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400720 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
721 if hasNextPageToken and hasPageToken:
722 createNextMethod(Resource, methodName + '_next',
723 resourceDesc['methods'][methodName],
724 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400725
726 return Resource()