blob: d5d693eeb334b5b179f20f2e29c155e373785922 [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 Gregorio7a6df3a2011-01-31 21:55:21 -0500248 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:
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400271 if value is None:
272 raise ValueError('String parameters can not be None.')
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500273 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500274 elif schema_type == 'integer':
275 return str(int(value))
276 elif schema_type == 'number':
277 return str(float(value))
278 elif schema_type == 'boolean':
279 return str(bool(value)).lower()
280 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500281 if type(value) == type('') or type(value) == type(u''):
282 return value
283 else:
284 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500285
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400286
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400287MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400288 "KB": 2 ** 10,
289 "MB": 2 ** 20,
290 "GB": 2 ** 30,
291 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400292 }
293
Joe Gregorioa98733f2011-09-16 10:12:28 -0400294
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400295def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400296 """Convert a string media size, such as 10GB or 3TB into an integer.
297
298 Args:
299 maxSize: string, size as a string, such as 2MB or 7GB.
300
301 Returns:
302 The size as an integer value.
303 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400304 if len(maxSize) < 2:
305 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400306 units = maxSize[-2:].upper()
307 multiplier = MULTIPLIERS.get(units, 0)
308 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400309 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400310 else:
311 return int(maxSize)
312
Joe Gregoriobee86832011-02-22 10:00:19 -0500313
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500314def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400315 developerKey, resourceDesc, rootDesc, schema):
316 """Build a Resource from the API description.
317
318 Args:
319 http: httplib2.Http, Object to make http requests with.
320 baseUrl: string, base URL for the API. All requests are relative to this
321 URI.
322 model: apiclient.Model, converts to and from the wire format.
323 requestBuilder: class or callable that instantiates an
324 apiclient.HttpRequest object.
325 developerKey: string, key obtained from
326 https://code.google.com/apis/console
327 resourceDesc: object, section of deserialized discovery document that
328 describes a resource. Note that the top level discovery document
329 is considered a resource.
330 rootDesc: object, the entire deserialized discovery document.
331 schema: object, mapping of schema names to schema descriptions.
332
333 Returns:
334 An instance of Resource with all the methods attached for interacting with
335 that resource.
336 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400337
338 class Resource(object):
339 """A class for interacting with a resource."""
340
341 def __init__(self):
342 self._http = http
343 self._baseUrl = baseUrl
344 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400345 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500346 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400347
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400348 def createMethod(theclass, methodName, methodDesc, rootDesc):
349 """Creates a method for attaching to a Resource.
350
351 Args:
352 theclass: type, the class to attach methods to.
353 methodName: string, name of the method to use.
354 methodDesc: object, fragment of deserialized discovery document that
355 describes the method.
356 rootDesc: object, the entire deserialized discovery document.
357 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400358 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400359 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400360 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400361 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400362
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400363 mediaPathUrl = None
364 accept = []
365 maxSize = 0
366 if 'mediaUpload' in methodDesc:
367 mediaUpload = methodDesc['mediaUpload']
Joe Gregoriode860442012-03-02 15:55:52 -0500368 # TODO(jcgregorio) Use URLs from discovery once it is updated.
369 parsed = list(urlparse.urlparse(baseUrl))
370 basePath = parsed[2]
371 mediaPathUrl = '/upload' + basePath + pathUrl
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400372 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400373 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400374
Joe Gregorioca876e42011-02-22 19:39:42 -0500375 if 'parameters' not in methodDesc:
376 methodDesc['parameters'] = {}
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400377
378 # Add in the parameters common to all methods.
379 for name, desc in rootDesc.get('parameters', {}).iteritems():
380 methodDesc['parameters'][name] = desc
381
382 # Add in undocumented query parameters.
Joe Gregorioca876e42011-02-22 19:39:42 -0500383 for name in STACK_QUERY_PARAMETERS:
384 methodDesc['parameters'][name] = {
385 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400386 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500387 }
388
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500389 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500390 methodDesc['parameters']['body'] = {
391 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500392 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500393 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500394 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500395 if 'request' in methodDesc:
396 methodDesc['parameters']['body'].update(methodDesc['request'])
397 else:
398 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500399 if 'mediaUpload' in methodDesc:
400 methodDesc['parameters']['media_body'] = {
401 'description': 'The filename of the media request body.',
402 'type': 'string',
403 'required': False,
404 }
405 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400406 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100407
Joe Gregorioca876e42011-02-22 19:39:42 -0500408 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100409 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500410 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100411 pattern_params = {} # Parameters that must match a regex
412 query_params = [] # Parameters that will be used in the query string
413 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500414 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500415 enum_params = {} # Allowable enumeration values for each parameter
416
417
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400418 if 'parameters' in methodDesc:
419 for arg, desc in methodDesc['parameters'].iteritems():
420 param = key2param(arg)
421 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400422
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400423 if desc.get('pattern', ''):
424 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500425 if desc.get('enum', ''):
426 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400427 if desc.get('required', False):
428 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500429 if desc.get('repeated', False):
430 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400431 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400432 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400433 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400434 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500435 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400436
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500437 for match in URITEMPLATE.finditer(pathUrl):
438 for namematch in VARNAME.finditer(match.group(0)):
439 name = key2param(namematch.group(0))
440 path_params[name] = name
441 if name in query_params:
442 query_params.remove(name)
443
Joe Gregorio48d361f2010-08-18 13:19:21 -0400444 def method(self, **kwargs):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400445 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400446 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500447 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400448 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400449
ade@google.com850cf552010-08-20 23:24:56 +0100450 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400451 if name not in kwargs:
452 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400453
ade@google.com850cf552010-08-20 23:24:56 +0100454 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400455 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500456 if isinstance(kwargs[name], basestring):
457 pvalues = [kwargs[name]]
458 else:
459 pvalues = kwargs[name]
460 for pvalue in pvalues:
461 if re.match(regex, pvalue) is None:
462 raise TypeError(
463 'Parameter "%s" value "%s" does not match the pattern "%s"' %
464 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400465
Joe Gregoriobee86832011-02-22 10:00:19 -0500466 for name, enums in enum_params.iteritems():
467 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800468 # We need to handle the case of a repeated enum
469 # name differently, since we want to handle both
470 # arg='value' and arg=['value1', 'value2']
471 if (name in repeated_params and
472 not isinstance(kwargs[name], basestring)):
473 values = kwargs[name]
474 else:
475 values = [kwargs[name]]
476 for value in values:
477 if value not in enums:
478 raise TypeError(
479 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
480 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500481
ade@google.com850cf552010-08-20 23:24:56 +0100482 actual_query_params = {}
483 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400484 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500485 to_type = param_type.get(key, 'string')
486 # For repeated parameters we cast each member of the list.
487 if key in repeated_params and type(value) == type([]):
488 cast_value = [_cast(x, to_type) for x in value]
489 else:
490 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100491 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500492 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100493 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500494 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100495 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400496 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400497
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400498 if self._developerKey:
499 actual_query_params['key'] = self._developerKey
500
Joe Gregorioe08a1662011-12-07 09:48:22 -0500501 model = self._model
502 # If there is no schema for the response then presume a binary blob.
Joe Gregorio708388c2012-06-15 13:43:04 -0400503 if methodName.endswith('_media'):
504 model = MediaModel()
505 elif 'response' not in methodDesc:
Joe Gregorioe08a1662011-12-07 09:48:22 -0500506 model = RawModel()
507
Joe Gregorio48d361f2010-08-18 13:19:21 -0400508 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500509 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400510 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400511
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400512 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400513 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
514
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500515 resumable = None
516 multipart_boundary = ''
517
Joe Gregorio922b78c2011-05-26 21:36:34 -0400518 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500519 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500520 if isinstance(media_filename, basestring):
521 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
522 if media_mime_type is None:
523 raise UnknownFileType(media_filename)
524 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
525 raise UnacceptableMimeTypeError(media_mime_type)
526 media_upload = MediaFileUpload(media_filename, media_mime_type)
527 elif isinstance(media_filename, MediaUpload):
528 media_upload = media_filename
529 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500530 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500531
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400532 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500533 if maxSize > 0 and media_upload.size() > maxSize:
534 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400535
536 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500537 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400538 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500539 if media_upload.resumable():
540 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400541
Joe Gregorio945be3e2012-01-27 17:01:06 -0500542 if media_upload.resumable():
543 # This is all we need to do for resumable, if the body exists it gets
544 # sent in the first request, otherwise an empty body is sent.
545 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400546 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500547 # A non-resumable upload
548 if body is None:
549 # This is a simple media upload
550 headers['content-type'] = media_upload.mimetype()
551 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500552 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500553 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500554 # This is a multipart/related upload.
555 msgRoot = MIMEMultipart('related')
556 # msgRoot should not write out it's own headers
557 setattr(msgRoot, '_write_headers', lambda self: None)
558
559 # attach the body as one part
560 msg = MIMENonMultipart(*headers['content-type'].split('/'))
561 msg.set_payload(body)
562 msgRoot.attach(msg)
563
564 # attach the media as the second part
565 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
566 msg['Content-Transfer-Encoding'] = 'binary'
567
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500568 payload = media_upload.getbytes(0, media_upload.size())
569 msg.set_payload(payload)
570 msgRoot.attach(msg)
571 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400572
Joe Gregorio945be3e2012-01-27 17:01:06 -0500573 multipart_boundary = msgRoot.get_boundary()
574 headers['content-type'] = ('multipart/related; '
575 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500576 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400577
Joe Gregorioe84c9442012-03-12 08:45:57 -0400578 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500579 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500580 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500581 url,
582 method=httpMethod,
583 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500584 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500585 methodId=methodId,
586 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400587
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500588 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
589 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500590 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400591
592 # Skip undocumented params and params common to all methods.
593 skip_parameters = rootDesc.get('parameters', {}).keys()
594 skip_parameters.append(STACK_QUERY_PARAMETERS)
595
Joe Gregorio48d361f2010-08-18 13:19:21 -0400596 for arg in argmap.iterkeys():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400597 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500598 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400599
Joe Gregorio61d7e962011-02-22 22:52:07 -0500600 repeated = ''
601 if arg in repeated_params:
602 repeated = ' (repeated)'
603 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400604 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500605 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500606 paramdesc = methodDesc['parameters'][argmap[arg]]
607 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500608 if '$ref' in paramdesc:
609 docs.append(
610 (' %s: object, %s%s%s\n The object takes the'
611 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
612 schema.prettyPrintByName(paramdesc['$ref'])))
613 else:
614 paramtype = paramdesc.get('type', 'string')
615 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
616 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500617 enum = paramdesc.get('enum', [])
618 enumDesc = paramdesc.get('enumDescriptions', [])
619 if enum and enumDesc:
620 docs.append(' Allowed values\n')
621 for (name, desc) in zip(enum, enumDesc):
622 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500623 if 'response' in methodDesc:
Joe Gregorio708388c2012-06-15 13:43:04 -0400624 if methodName.endswith('_media'):
625 docs.append('\nReturns:\n The media object as a string.\n\n ')
626 else:
627 docs.append('\nReturns:\n An object of the form:\n\n ')
628 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400629
630 setattr(method, '__doc__', ''.join(docs))
631 setattr(theclass, methodName, method)
632
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400633 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
634 """Creates any _next methods for attaching to a Resource.
635
636 The _next methods allow for easy iteration through list() responses.
637
638 Args:
639 theclass: type, the class to attach methods to.
640 methodName: string, name of the method to use.
641 methodDesc: object, fragment of deserialized discovery document that
642 describes the method.
643 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400644 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400645 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400646 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400647
Joe Gregorio3c676f92011-07-25 10:38:14 -0400648 def methodNext(self, previous_request, previous_response):
649 """Retrieves the next page of results.
650
651 Args:
652 previous_request: The request for the previous page.
653 previous_response: The response from the request for the previous page.
654
655 Returns:
656 A request object that you can call 'execute()' on to request the next
657 page. Returns None if there are no more items in the collection.
658 """
659 # Retrieve nextPageToken from previous_response
660 # Use as pageToken in previous_request to create new request.
661
662 if 'nextPageToken' not in previous_response:
663 return None
664
665 request = copy.copy(previous_request)
666
667 pageToken = previous_response['nextPageToken']
668 parsed = list(urlparse.urlparse(request.uri))
669 q = parse_qsl(parsed[4])
670
671 # Find and remove old 'pageToken' value from URI
672 newq = [(key, value) for (key, value) in q if key != 'pageToken']
673 newq.append(('pageToken', pageToken))
674 parsed[4] = urllib.urlencode(newq)
675 uri = urlparse.urlunparse(parsed)
676
677 request.uri = uri
678
Joe Gregorioe84c9442012-03-12 08:45:57 -0400679 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400680
681 return request
682
683 setattr(theclass, methodName, methodNext)
684
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400685 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400686 if 'methods' in resourceDesc:
687 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400688 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio708388c2012-06-15 13:43:04 -0400689 # Add in _media methods. The functionality of the attached method will
690 # change when it sees that the method name ends in _media.
691 if methodDesc.get('supportsMediaDownload', False):
692 createMethod(Resource, methodName + '_media', methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400693
694 # Add in nested resources
695 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500696
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400697 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
698 """Create a method on the Resource to access a nested Resource.
699
700 Args:
701 theclass: type, the class to attach methods to.
702 methodName: string, name of the method to use.
703 methodDesc: object, fragment of deserialized discovery document that
704 describes the method.
705 rootDesc: object, the entire deserialized discovery document.
706 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400707 methodName = fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400708
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500709 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400710 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500711 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400712 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400713
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500714 setattr(methodResource, '__doc__', 'A collection resource.')
715 setattr(methodResource, '__is_resource__', True)
716 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400717
718 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400719 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400720
Joe Gregorio3c676f92011-07-25 10:38:14 -0400721 # Add _next() methods
722 # Look for response bodies in schema that contain nextPageToken, and methods
723 # that take a pageToken parameter.
724 if 'methods' in resourceDesc:
725 for methodName, methodDesc in resourceDesc['methods'].iteritems():
726 if 'response' in methodDesc:
727 responseSchema = methodDesc['response']
728 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500729 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400730 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
731 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400732 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
733 if hasNextPageToken and hasPageToken:
734 createNextMethod(Resource, methodName + '_next',
735 resourceDesc['methods'][methodName],
736 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400737
738 return Resource()