blob: d3b64694461090c2d3f817d3ddf11dac519fd8b5 [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 Gregoriof4839b02012-09-06 13:47:24 -040060from oauth2client.util import positional
Joe Gregorio549230c2012-01-11 10:38:05 -050061from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050062
Joe Gregorioe84c9442012-03-12 08:45:57 -040063logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040064
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050065URITEMPLATE = re.compile('{[^}]*}')
66VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040067DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
68 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050069DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050070
Joe Gregorioc8e421c2012-06-06 14:03:13 -040071# Parameters accepted by the stack, but not visible via discovery.
72STACK_QUERY_PARAMETERS = ['trace', 'pp', 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040073
Joe Gregorioc8e421c2012-06-06 14:03:13 -040074# Python reserved words.
Joe Gregorio562b7312011-09-15 09:06:38 -040075RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040076 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
77 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
Joe Gregorio81d92cc2012-07-09 16:46:02 -040078 'pass', 'print', 'raise', 'return', 'try', 'while', 'body']
Joe Gregoriod92897c2011-07-07 11:44:56 -040079
Joe Gregorio562b7312011-09-15 09:06:38 -040080
Joe Gregorioce31a972012-06-06 15:48:17 -040081def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040082 """Fix method names to avoid reserved word conflicts.
83
84 Args:
85 name: string, method name.
86
87 Returns:
88 The name with a '_' prefixed if the name is a reserved word.
89 """
Joe Gregoriod92897c2011-07-07 11:44:56 -040090 if name in RESERVED_WORDS:
91 return name + '_'
92 else:
93 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040094
Joe Gregorioa98733f2011-09-16 10:12:28 -040095
Joe Gregorioa98733f2011-09-16 10:12:28 -040096def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050097 """Adds a query parameter to a url.
98
99 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400100
101 Args:
102 url: string, url to add the query parameter to.
103 name: string, query parameter name.
104 value: string, query parameter value.
105
106 Returns:
107 Updated query parameter. Does not update the url if value is None.
108 """
109 if value is None:
110 return url
111 else:
112 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500113 q = dict(parse_qsl(parsed[4]))
114 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400115 parsed[4] = urllib.urlencode(q)
116 return urlparse.urlunparse(parsed)
117
118
Joe Gregorio48d361f2010-08-18 13:19:21 -0400119def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500120 """Converts key names into parameter names.
121
122 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400123
124 Args:
125 key: string, the method key name.
126
127 Returns:
128 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400129 """
130 result = []
131 key = list(key)
132 if not key[0].isalpha():
133 result.append('x')
134 for c in key:
135 if c.isalnum():
136 result.append(c)
137 else:
138 result.append('_')
139
140 return ''.join(result)
141
142
Joe Gregoriof4839b02012-09-06 13:47:24 -0400143@positional(2)
Joe Gregorio01770a52012-02-24 11:11:10 -0500144def build(serviceName,
145 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500146 http=None,
147 discoveryServiceUrl=DISCOVERY_URI,
148 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500149 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500150 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500151 """Construct a Resource for interacting with an API.
152
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400153 Construct a Resource object for interacting with an API. The serviceName and
154 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500155
156 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400157 serviceName: string, name of the service.
158 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500159 http: httplib2.Http, An instance of httplib2.Http or something that acts
160 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400161 discoveryServiceUrl: string, a URI Template that points to the location of
162 the discovery service. It should have two parameters {api} and
163 {apiVersion} that when filled in produce an absolute URI to the discovery
164 document for that service.
165 developerKey: string, key obtained from
166 https://code.google.com/apis/console.
167 model: apiclient.Model, converts to and from the wire format.
168 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
169 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500170
171 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400172 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500173 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400174 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400175 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400176 'apiVersion': version
177 }
ade@google.com850cf552010-08-20 23:24:56 +0100178
Joe Gregorioc204b642010-09-21 12:01:23 -0400179 if http is None:
180 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400181
ade@google.com850cf552010-08-20 23:24:56 +0100182 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400183
Joe Gregorio66f57522011-11-30 11:00:00 -0500184 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
185 # variable that contains the network address of the client sending the
186 # request. If it exists then add that to the request for the discovery
187 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400188 if 'REMOTE_ADDR' in os.environ:
189 requested_url = _add_query_parameter(requested_url, 'userIp',
190 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400191 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400192
ade@google.com850cf552010-08-20 23:24:56 +0100193 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400194
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500195 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500196 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500197 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400198 if resp.status >= 400:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400199 raise HttpError(resp, content, uri=requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400200
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500201 try:
202 service = simplejson.loads(content)
203 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400204 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500205 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400206
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400207 return build_from_document(content, base=discoveryServiceUrl, http=http,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400208 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500209
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500210
Joe Gregoriof4839b02012-09-06 13:47:24 -0400211@positional(1)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500212def build_from_document(
213 service,
Joe Gregorioa2838152012-07-16 11:52:17 -0400214 base=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500215 future=None,
216 http=None,
217 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500218 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500219 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500220 """Create a Resource for interacting with an API.
221
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400222 Same as `build()`, but constructs the Resource object from a discovery
223 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500224
Joe Gregorio292b9b82011-01-12 11:36:11 -0500225 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400226 service: string, discovery document.
227 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400228 This parameter is no longer used as rootUrl and servicePath are included
229 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400230 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500231 http: httplib2.Http, An instance of httplib2.Http or something that acts
232 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500233 developerKey: string, Key for controlling API usage, generated
234 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400235 model: Model class instance that serializes and de-serializes requests and
236 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500237 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500238
239 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400240 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500241 """
242
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400243 # future is no longer used.
244 future = {}
245
Joe Gregorio292b9b82011-01-12 11:36:11 -0500246 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400247 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500248 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400249
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500250 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500251 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500252 model = JsonModel('dataWrapper' in features)
Joe Gregorioebd0b842012-06-15 14:14:17 -0400253 resource = _createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400254 service, service, schema)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400255
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500256 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400257
258
Joe Gregorio61d7e962011-02-22 22:52:07 -0500259def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500260 """Convert value to a string based on JSON Schema type.
261
262 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
263 JSON Schema.
264
265 Args:
266 value: any, the value to convert
267 schema_type: string, the type that value should be interpreted as
268
269 Returns:
270 A string representation of 'value' based on the schema_type.
271 """
272 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500273 if type(value) == type('') or type(value) == type(u''):
274 return value
275 else:
276 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500277 elif schema_type == 'integer':
278 return str(int(value))
279 elif schema_type == 'number':
280 return str(float(value))
281 elif schema_type == 'boolean':
282 return str(bool(value)).lower()
283 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500284 if type(value) == type('') or type(value) == type(u''):
285 return value
286 else:
287 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500288
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400289
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400290MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400291 "KB": 2 ** 10,
292 "MB": 2 ** 20,
293 "GB": 2 ** 30,
294 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400295 }
296
Joe Gregorioa98733f2011-09-16 10:12:28 -0400297
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400298def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400299 """Convert a string media size, such as 10GB or 3TB into an integer.
300
301 Args:
302 maxSize: string, size as a string, such as 2MB or 7GB.
303
304 Returns:
305 The size as an integer value.
306 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400307 if len(maxSize) < 2:
308 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400309 units = maxSize[-2:].upper()
310 multiplier = MULTIPLIERS.get(units, 0)
311 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400312 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400313 else:
314 return int(maxSize)
315
Joe Gregoriobee86832011-02-22 10:00:19 -0500316
Joe Gregorioebd0b842012-06-15 14:14:17 -0400317def _createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400318 developerKey, resourceDesc, rootDesc, schema):
319 """Build a Resource from the API description.
320
321 Args:
322 http: httplib2.Http, Object to make http requests with.
323 baseUrl: string, base URL for the API. All requests are relative to this
324 URI.
325 model: apiclient.Model, converts to and from the wire format.
326 requestBuilder: class or callable that instantiates an
327 apiclient.HttpRequest object.
328 developerKey: string, key obtained from
329 https://code.google.com/apis/console
330 resourceDesc: object, section of deserialized discovery document that
331 describes a resource. Note that the top level discovery document
332 is considered a resource.
333 rootDesc: object, the entire deserialized discovery document.
334 schema: object, mapping of schema names to schema descriptions.
335
336 Returns:
337 An instance of Resource with all the methods attached for interacting with
338 that resource.
339 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400340
341 class Resource(object):
342 """A class for interacting with a resource."""
343
344 def __init__(self):
345 self._http = http
346 self._baseUrl = baseUrl
347 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400348 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500349 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400350
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400351 def createMethod(theclass, methodName, methodDesc, rootDesc):
352 """Creates a method for attaching to a Resource.
353
354 Args:
355 theclass: type, the class to attach methods to.
356 methodName: string, name of the method to use.
357 methodDesc: object, fragment of deserialized discovery document that
358 describes the method.
359 rootDesc: object, the entire deserialized discovery document.
360 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400361 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400362 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400363 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400364 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400365
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400366 mediaPathUrl = None
367 accept = []
368 maxSize = 0
369 if 'mediaUpload' in methodDesc:
370 mediaUpload = methodDesc['mediaUpload']
Joe Gregorioa2838152012-07-16 11:52:17 -0400371 mediaPathUrl = (rootDesc['rootUrl'] + 'upload/' + rootDesc['servicePath']
372 + pathUrl)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400373 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400374 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400375
Joe Gregorioca876e42011-02-22 19:39:42 -0500376 if 'parameters' not in methodDesc:
377 methodDesc['parameters'] = {}
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400378
379 # Add in the parameters common to all methods.
380 for name, desc in rootDesc.get('parameters', {}).iteritems():
381 methodDesc['parameters'][name] = desc
382
383 # Add in undocumented query parameters.
Joe Gregorioca876e42011-02-22 19:39:42 -0500384 for name in STACK_QUERY_PARAMETERS:
385 methodDesc['parameters'][name] = {
386 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400387 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500388 }
389
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500390 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500391 methodDesc['parameters']['body'] = {
392 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500393 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500394 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500395 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500396 if 'request' in methodDesc:
397 methodDesc['parameters']['body'].update(methodDesc['request'])
398 else:
399 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500400 if 'mediaUpload' in methodDesc:
401 methodDesc['parameters']['media_body'] = {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400402 'description':
403 'The filename of the media request body, or an instance of a '
404 'MediaUpload object.',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500405 'type': 'string',
406 'required': False,
407 }
408 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400409 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100410
Joe Gregorioca876e42011-02-22 19:39:42 -0500411 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100412 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500413 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100414 pattern_params = {} # Parameters that must match a regex
415 query_params = [] # Parameters that will be used in the query string
416 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500417 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500418 enum_params = {} # Allowable enumeration values for each parameter
419
420
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400421 if 'parameters' in methodDesc:
422 for arg, desc in methodDesc['parameters'].iteritems():
423 param = key2param(arg)
424 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400425
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400426 if desc.get('pattern', ''):
427 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500428 if desc.get('enum', ''):
429 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400430 if desc.get('required', False):
431 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500432 if desc.get('repeated', False):
433 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400434 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400435 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400436 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400437 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500438 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400439
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500440 for match in URITEMPLATE.finditer(pathUrl):
441 for namematch in VARNAME.finditer(match.group(0)):
442 name = key2param(namematch.group(0))
443 path_params[name] = name
444 if name in query_params:
445 query_params.remove(name)
446
Joe Gregorio48d361f2010-08-18 13:19:21 -0400447 def method(self, **kwargs):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400448 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorio2467afa2012-06-20 12:21:25 -0400449
Joe Gregorio48d361f2010-08-18 13:19:21 -0400450 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500451 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400452 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400453
Joe Gregorio2467afa2012-06-20 12:21:25 -0400454 # Remove args that have a value of None.
455 keys = kwargs.keys()
456 for name in keys:
457 if kwargs[name] is None:
458 del kwargs[name]
459
ade@google.com850cf552010-08-20 23:24:56 +0100460 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400461 if name not in kwargs:
462 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400463
ade@google.com850cf552010-08-20 23:24:56 +0100464 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400465 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500466 if isinstance(kwargs[name], basestring):
467 pvalues = [kwargs[name]]
468 else:
469 pvalues = kwargs[name]
470 for pvalue in pvalues:
471 if re.match(regex, pvalue) is None:
472 raise TypeError(
473 'Parameter "%s" value "%s" does not match the pattern "%s"' %
474 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400475
Joe Gregoriobee86832011-02-22 10:00:19 -0500476 for name, enums in enum_params.iteritems():
477 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800478 # We need to handle the case of a repeated enum
479 # name differently, since we want to handle both
480 # arg='value' and arg=['value1', 'value2']
481 if (name in repeated_params and
482 not isinstance(kwargs[name], basestring)):
483 values = kwargs[name]
484 else:
485 values = [kwargs[name]]
486 for value in values:
487 if value not in enums:
488 raise TypeError(
489 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
490 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500491
ade@google.com850cf552010-08-20 23:24:56 +0100492 actual_query_params = {}
493 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400494 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500495 to_type = param_type.get(key, 'string')
496 # For repeated parameters we cast each member of the list.
497 if key in repeated_params and type(value) == type([]):
498 cast_value = [_cast(x, to_type) for x in value]
499 else:
500 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100501 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500502 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100503 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500504 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100505 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400506 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400507
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400508 if self._developerKey:
509 actual_query_params['key'] = self._developerKey
510
Joe Gregorioe08a1662011-12-07 09:48:22 -0500511 model = self._model
Joe Gregorio708388c2012-06-15 13:43:04 -0400512 if methodName.endswith('_media'):
513 model = MediaModel()
514 elif 'response' not in methodDesc:
Joe Gregorioe08a1662011-12-07 09:48:22 -0500515 model = RawModel()
516
Joe Gregorio48d361f2010-08-18 13:19:21 -0400517 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500518 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400519 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400520
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400521 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400522 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
523
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500524 resumable = None
525 multipart_boundary = ''
526
Joe Gregorio922b78c2011-05-26 21:36:34 -0400527 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500528 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500529 if isinstance(media_filename, basestring):
530 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
531 if media_mime_type is None:
532 raise UnknownFileType(media_filename)
533 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
534 raise UnacceptableMimeTypeError(media_mime_type)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400535 media_upload = MediaFileUpload(media_filename,
536 mimetype=media_mime_type)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500537 elif isinstance(media_filename, MediaUpload):
538 media_upload = media_filename
539 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500540 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500541
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400542 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500543 if maxSize > 0 and media_upload.size() > maxSize:
544 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400545
546 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500547 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400548 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500549 if media_upload.resumable():
550 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400551
Joe Gregorio945be3e2012-01-27 17:01:06 -0500552 if media_upload.resumable():
553 # This is all we need to do for resumable, if the body exists it gets
554 # sent in the first request, otherwise an empty body is sent.
555 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400556 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500557 # A non-resumable upload
558 if body is None:
559 # This is a simple media upload
560 headers['content-type'] = media_upload.mimetype()
561 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500562 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500563 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500564 # This is a multipart/related upload.
565 msgRoot = MIMEMultipart('related')
566 # msgRoot should not write out it's own headers
567 setattr(msgRoot, '_write_headers', lambda self: None)
568
569 # attach the body as one part
570 msg = MIMENonMultipart(*headers['content-type'].split('/'))
571 msg.set_payload(body)
572 msgRoot.attach(msg)
573
574 # attach the media as the second part
575 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
576 msg['Content-Transfer-Encoding'] = 'binary'
577
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500578 payload = media_upload.getbytes(0, media_upload.size())
579 msg.set_payload(payload)
580 msgRoot.attach(msg)
581 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400582
Joe Gregorio945be3e2012-01-27 17:01:06 -0500583 multipart_boundary = msgRoot.get_boundary()
584 headers['content-type'] = ('multipart/related; '
585 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500586 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400587
Joe Gregorioe84c9442012-03-12 08:45:57 -0400588 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500589 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500590 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500591 url,
592 method=httpMethod,
593 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500594 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500595 methodId=methodId,
596 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400597
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500598 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
599 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500600 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400601
602 # Skip undocumented params and params common to all methods.
603 skip_parameters = rootDesc.get('parameters', {}).keys()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400604 skip_parameters.extend(STACK_QUERY_PARAMETERS)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400605
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400606 all_args = argmap.keys()
607 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
608
609 # Move body to the front of the line.
610 if 'body' in all_args:
611 args_ordered.append('body')
612
613 for name in all_args:
614 if name not in args_ordered:
615 args_ordered.append(name)
616
617 for arg in args_ordered:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400618 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500619 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400620
Joe Gregorio61d7e962011-02-22 22:52:07 -0500621 repeated = ''
622 if arg in repeated_params:
623 repeated = ' (repeated)'
624 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400625 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500626 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500627 paramdesc = methodDesc['parameters'][argmap[arg]]
628 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500629 if '$ref' in paramdesc:
630 docs.append(
631 (' %s: object, %s%s%s\n The object takes the'
632 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
633 schema.prettyPrintByName(paramdesc['$ref'])))
634 else:
635 paramtype = paramdesc.get('type', 'string')
636 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
637 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500638 enum = paramdesc.get('enum', [])
639 enumDesc = paramdesc.get('enumDescriptions', [])
640 if enum and enumDesc:
641 docs.append(' Allowed values\n')
642 for (name, desc) in zip(enum, enumDesc):
643 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500644 if 'response' in methodDesc:
Joe Gregorio708388c2012-06-15 13:43:04 -0400645 if methodName.endswith('_media'):
646 docs.append('\nReturns:\n The media object as a string.\n\n ')
647 else:
648 docs.append('\nReturns:\n An object of the form:\n\n ')
649 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400650
651 setattr(method, '__doc__', ''.join(docs))
652 setattr(theclass, methodName, method)
653
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400654 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
655 """Creates any _next methods for attaching to a Resource.
656
657 The _next methods allow for easy iteration through list() responses.
658
659 Args:
660 theclass: type, the class to attach methods to.
661 methodName: string, name of the method to use.
662 methodDesc: object, fragment of deserialized discovery document that
663 describes the method.
664 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400665 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400666 methodName = fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400667 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400668
Joe Gregorio3c676f92011-07-25 10:38:14 -0400669 def methodNext(self, previous_request, previous_response):
670 """Retrieves the next page of results.
671
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400672Args:
673 previous_request: The request for the previous page. (required)
674 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400675
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400676Returns:
677 A request object that you can call 'execute()' on to request the next
678 page. Returns None if there are no more items in the collection.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400679 """
680 # Retrieve nextPageToken from previous_response
681 # Use as pageToken in previous_request to create new request.
682
683 if 'nextPageToken' not in previous_response:
684 return None
685
686 request = copy.copy(previous_request)
687
688 pageToken = previous_response['nextPageToken']
689 parsed = list(urlparse.urlparse(request.uri))
690 q = parse_qsl(parsed[4])
691
692 # Find and remove old 'pageToken' value from URI
693 newq = [(key, value) for (key, value) in q if key != 'pageToken']
694 newq.append(('pageToken', pageToken))
695 parsed[4] = urllib.urlencode(newq)
696 uri = urlparse.urlunparse(parsed)
697
698 request.uri = uri
699
Joe Gregorioe84c9442012-03-12 08:45:57 -0400700 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400701
702 return request
703
704 setattr(theclass, methodName, methodNext)
705
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400706 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400707 if 'methods' in resourceDesc:
708 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400709 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio708388c2012-06-15 13:43:04 -0400710 # Add in _media methods. The functionality of the attached method will
711 # change when it sees that the method name ends in _media.
712 if methodDesc.get('supportsMediaDownload', False):
713 createMethod(Resource, methodName + '_media', methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400714
715 # Add in nested resources
716 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500717
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400718 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
719 """Create a method on the Resource to access a nested Resource.
720
721 Args:
722 theclass: type, the class to attach methods to.
723 methodName: string, name of the method to use.
724 methodDesc: object, fragment of deserialized discovery document that
725 describes the method.
726 rootDesc: object, the entire deserialized discovery document.
727 """
Joe Gregorioce31a972012-06-06 15:48:17 -0400728 methodName = fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400729
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500730 def methodResource(self):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400731 return _createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500732 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400733 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400734
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500735 setattr(methodResource, '__doc__', 'A collection resource.')
736 setattr(methodResource, '__is_resource__', True)
737 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400738
739 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400740 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400741
Joe Gregorio3c676f92011-07-25 10:38:14 -0400742 # Add _next() methods
743 # Look for response bodies in schema that contain nextPageToken, and methods
744 # that take a pageToken parameter.
745 if 'methods' in resourceDesc:
746 for methodName, methodDesc in resourceDesc['methods'].iteritems():
747 if 'response' in methodDesc:
748 responseSchema = methodDesc['response']
749 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500750 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400751 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
752 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400753 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
754 if hasNextPageToken and hasPageToken:
755 createNextMethod(Resource, methodName + '_next',
756 resourceDesc['methods'][methodName],
757 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400758
759 return Resource()