blob: 3d46758b988025c9d0986c4076048457f09c7219 [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__ = [
22 'build', 'build_from_document'
23 ]
Joe Gregorio48d361f2010-08-18 13:19:21 -040024
Joe Gregorio3c676f92011-07-25 10:38:14 -040025import copy
Joe Gregorio48d361f2010-08-18 13:19:21 -040026import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010027import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040028import os
Joe Gregoriod0bd3882011-11-22 09:49:47 -050029import random
Joe Gregorio48d361f2010-08-18 13:19:21 -040030import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040031import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040032import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040033import urlparse
Joe Gregoriofdf7c802011-06-30 12:33:38 -040034import mimeparse
Joe Gregorio922b78c2011-05-26 21:36:34 -040035import mimetypes
36
ade@google.comc5eb46f2010-09-27 23:35:39 +010037try:
38 from urlparse import parse_qsl
39except ImportError:
40 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050041
Joe Gregorio2b781282011-12-08 12:00:25 -050042from apiclient.errors import HttpError
43from apiclient.errors import InvalidJsonError
44from apiclient.errors import MediaUploadSizeError
45from apiclient.errors import UnacceptableMimeTypeError
46from apiclient.errors import UnknownApiNameOrVersion
47from apiclient.errors import UnknownLinkType
48from apiclient.http import HttpRequest
49from apiclient.http import MediaFileUpload
50from apiclient.http import MediaUpload
51from apiclient.model import JsonModel
52from apiclient.model import RawModel
53from apiclient.schema import Schemas
Joe Gregorio922b78c2011-05-26 21:36:34 -040054from email.mime.multipart import MIMEMultipart
55from email.mime.nonmultipart import MIMENonMultipart
Joe Gregorio549230c2012-01-11 10:38:05 -050056from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050057
Joe Gregorioe84c9442012-03-12 08:45:57 -040058logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040059
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050060URITEMPLATE = re.compile('{[^}]*}')
61VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040062DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
63 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050064DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050065
Joe Gregorioc8e421c2012-06-06 14:03:13 -040066# Parameters accepted by the stack, but not visible via discovery.
67STACK_QUERY_PARAMETERS = ['trace', 'pp', 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040068
Joe Gregorioc8e421c2012-06-06 14:03:13 -040069# Python reserved words.
Joe Gregorio562b7312011-09-15 09:06:38 -040070RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040071 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
72 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
73 'pass', 'print', 'raise', 'return', 'try', 'while' ]
74
Joe Gregorio562b7312011-09-15 09:06:38 -040075
Joe Gregoriod92897c2011-07-07 11:44:56 -040076def _fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040077 """Fix method names to avoid reserved word conflicts.
78
79 Args:
80 name: string, method name.
81
82 Returns:
83 The name with a '_' prefixed if the name is a reserved word.
84 """
Joe Gregoriod92897c2011-07-07 11:44:56 -040085 if name in RESERVED_WORDS:
86 return name + '_'
87 else:
88 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040089
Joe Gregorioa98733f2011-09-16 10:12:28 -040090
Joe Gregorioa98733f2011-09-16 10:12:28 -040091def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050092 """Adds a query parameter to a url.
93
94 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -040095
96 Args:
97 url: string, url to add the query parameter to.
98 name: string, query parameter name.
99 value: string, query parameter value.
100
101 Returns:
102 Updated query parameter. Does not update the url if value is None.
103 """
104 if value is None:
105 return url
106 else:
107 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500108 q = dict(parse_qsl(parsed[4]))
109 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400110 parsed[4] = urllib.urlencode(q)
111 return urlparse.urlunparse(parsed)
112
113
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500115 """Converts key names into parameter names.
116
117 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400118
119 Args:
120 key: string, the method key name.
121
122 Returns:
123 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400124 """
125 result = []
126 key = list(key)
127 if not key[0].isalpha():
128 result.append('x')
129 for c in key:
130 if c.isalnum():
131 result.append(c)
132 else:
133 result.append('_')
134
135 return ''.join(result)
136
137
Joe Gregorio01770a52012-02-24 11:11:10 -0500138def build(serviceName,
139 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500140 http=None,
141 discoveryServiceUrl=DISCOVERY_URI,
142 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500143 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500144 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500145 """Construct a Resource for interacting with an API.
146
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400147 Construct a Resource object for interacting with an API. The serviceName and
148 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500149
150 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400151 serviceName: string, name of the service.
152 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500153 http: httplib2.Http, An instance of httplib2.Http or something that acts
154 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400155 discoveryServiceUrl: string, a URI Template that points to the location of
156 the discovery service. It should have two parameters {api} and
157 {apiVersion} that when filled in produce an absolute URI to the discovery
158 document for that service.
159 developerKey: string, key obtained from
160 https://code.google.com/apis/console.
161 model: apiclient.Model, converts to and from the wire format.
162 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
163 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500164
165 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400166 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500167 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400168 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400169 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400170 'apiVersion': version
171 }
ade@google.com850cf552010-08-20 23:24:56 +0100172
Joe Gregorioc204b642010-09-21 12:01:23 -0400173 if http is None:
174 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400175
ade@google.com850cf552010-08-20 23:24:56 +0100176 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400177
Joe Gregorio66f57522011-11-30 11:00:00 -0500178 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
179 # variable that contains the network address of the client sending the
180 # request. If it exists then add that to the request for the discovery
181 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400182 if 'REMOTE_ADDR' in os.environ:
183 requested_url = _add_query_parameter(requested_url, 'userIp',
184 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400185 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400186
ade@google.com850cf552010-08-20 23:24:56 +0100187 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400188
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500189 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500190 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500191 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400192 if resp.status >= 400:
Joe Gregorio49396552011-03-08 10:39:00 -0500193 raise HttpError(resp, content, requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400194
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500195 try:
196 service = simplejson.loads(content)
197 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400198 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500199 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400200
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400201 return build_from_document(content, discoveryServiceUrl, http=http,
202 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500203
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500204
Joe Gregorio292b9b82011-01-12 11:36:11 -0500205def build_from_document(
206 service,
207 base,
208 future=None,
209 http=None,
210 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500211 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500212 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500213 """Create a Resource for interacting with an API.
214
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400215 Same as `build()`, but constructs the Resource object from a discovery
216 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500217
Joe Gregorio292b9b82011-01-12 11:36:11 -0500218 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400219 service: string, discovery document.
220 base: string, base URI for all HTTP requests, usually the discovery URI.
221 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500222 http: httplib2.Http, An instance of httplib2.Http or something that acts
223 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500224 developerKey: string, Key for controlling API usage, generated
225 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400226 model: Model class instance that serializes and de-serializes requests and
227 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500228 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500229
230 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400231 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500232 """
233
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400234 # future is no longer used.
235 future = {}
236
Joe Gregorio292b9b82011-01-12 11:36:11 -0500237 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400238 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500239 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400240
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500241 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500242 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500243 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500244 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400245 service, service, schema)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400246
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500247 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400248
249
Joe Gregorio61d7e962011-02-22 22:52:07 -0500250def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500251 """Convert value to a string based on JSON Schema type.
252
253 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
254 JSON Schema.
255
256 Args:
257 value: any, the value to convert
258 schema_type: string, the type that value should be interpreted as
259
260 Returns:
261 A string representation of 'value' based on the schema_type.
262 """
263 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500264 if type(value) == type('') or type(value) == type(u''):
265 return value
266 else:
267 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500268 elif schema_type == 'integer':
269 return str(int(value))
270 elif schema_type == 'number':
271 return str(float(value))
272 elif schema_type == 'boolean':
273 return str(bool(value)).lower()
274 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500275 if type(value) == type('') or type(value) == type(u''):
276 return value
277 else:
278 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500279
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400280
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400281MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400282 "KB": 2 ** 10,
283 "MB": 2 ** 20,
284 "GB": 2 ** 30,
285 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400286 }
287
Joe Gregorioa98733f2011-09-16 10:12:28 -0400288
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400289def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400290 """Convert a string media size, such as 10GB or 3TB into an integer.
291
292 Args:
293 maxSize: string, size as a string, such as 2MB or 7GB.
294
295 Returns:
296 The size as an integer value.
297 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400298 if len(maxSize) < 2:
299 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400300 units = maxSize[-2:].upper()
301 multiplier = MULTIPLIERS.get(units, 0)
302 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400303 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400304 else:
305 return int(maxSize)
306
Joe Gregoriobee86832011-02-22 10:00:19 -0500307
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500308def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400309 developerKey, resourceDesc, rootDesc, schema):
310 """Build a Resource from the API description.
311
312 Args:
313 http: httplib2.Http, Object to make http requests with.
314 baseUrl: string, base URL for the API. All requests are relative to this
315 URI.
316 model: apiclient.Model, converts to and from the wire format.
317 requestBuilder: class or callable that instantiates an
318 apiclient.HttpRequest object.
319 developerKey: string, key obtained from
320 https://code.google.com/apis/console
321 resourceDesc: object, section of deserialized discovery document that
322 describes a resource. Note that the top level discovery document
323 is considered a resource.
324 rootDesc: object, the entire deserialized discovery document.
325 schema: object, mapping of schema names to schema descriptions.
326
327 Returns:
328 An instance of Resource with all the methods attached for interacting with
329 that resource.
330 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400331
332 class Resource(object):
333 """A class for interacting with a resource."""
334
335 def __init__(self):
336 self._http = http
337 self._baseUrl = baseUrl
338 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400339 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500340 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400341
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400342 def createMethod(theclass, methodName, methodDesc, rootDesc):
343 """Creates a method for attaching to a Resource.
344
345 Args:
346 theclass: type, the class to attach methods to.
347 methodName: string, name of the method to use.
348 methodDesc: object, fragment of deserialized discovery document that
349 describes the method.
350 rootDesc: object, the entire deserialized discovery document.
351 """
Joe Gregoriod92897c2011-07-07 11:44:56 -0400352 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400353 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400354 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400355 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400356
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400357 mediaPathUrl = None
358 accept = []
359 maxSize = 0
360 if 'mediaUpload' in methodDesc:
361 mediaUpload = methodDesc['mediaUpload']
Joe Gregoriode860442012-03-02 15:55:52 -0500362 # TODO(jcgregorio) Use URLs from discovery once it is updated.
363 parsed = list(urlparse.urlparse(baseUrl))
364 basePath = parsed[2]
365 mediaPathUrl = '/upload' + basePath + pathUrl
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400366 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400367 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400368
Joe Gregorioca876e42011-02-22 19:39:42 -0500369 if 'parameters' not in methodDesc:
370 methodDesc['parameters'] = {}
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400371
372 # Add in the parameters common to all methods.
373 for name, desc in rootDesc.get('parameters', {}).iteritems():
374 methodDesc['parameters'][name] = desc
375
376 # Add in undocumented query parameters.
Joe Gregorioca876e42011-02-22 19:39:42 -0500377 for name in STACK_QUERY_PARAMETERS:
378 methodDesc['parameters'][name] = {
379 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400380 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500381 }
382
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500383 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500384 methodDesc['parameters']['body'] = {
385 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500386 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500387 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500388 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500389 if 'request' in methodDesc:
390 methodDesc['parameters']['body'].update(methodDesc['request'])
391 else:
392 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500393 if 'mediaUpload' in methodDesc:
394 methodDesc['parameters']['media_body'] = {
395 'description': 'The filename of the media request body.',
396 'type': 'string',
397 'required': False,
398 }
399 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400400 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100401
Joe Gregorioca876e42011-02-22 19:39:42 -0500402 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100403 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500404 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100405 pattern_params = {} # Parameters that must match a regex
406 query_params = [] # Parameters that will be used in the query string
407 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500408 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500409 enum_params = {} # Allowable enumeration values for each parameter
410
411
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400412 if 'parameters' in methodDesc:
413 for arg, desc in methodDesc['parameters'].iteritems():
414 param = key2param(arg)
415 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400416
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400417 if desc.get('pattern', ''):
418 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500419 if desc.get('enum', ''):
420 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400421 if desc.get('required', False):
422 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500423 if desc.get('repeated', False):
424 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400425 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400426 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400427 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400428 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500429 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400430
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500431 for match in URITEMPLATE.finditer(pathUrl):
432 for namematch in VARNAME.finditer(match.group(0)):
433 name = key2param(namematch.group(0))
434 path_params[name] = name
435 if name in query_params:
436 query_params.remove(name)
437
Joe Gregorio48d361f2010-08-18 13:19:21 -0400438 def method(self, **kwargs):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400439 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400440 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500441 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400442 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400443
ade@google.com850cf552010-08-20 23:24:56 +0100444 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400445 if name not in kwargs:
446 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400447
ade@google.com850cf552010-08-20 23:24:56 +0100448 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400449 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500450 if isinstance(kwargs[name], basestring):
451 pvalues = [kwargs[name]]
452 else:
453 pvalues = kwargs[name]
454 for pvalue in pvalues:
455 if re.match(regex, pvalue) is None:
456 raise TypeError(
457 'Parameter "%s" value "%s" does not match the pattern "%s"' %
458 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400459
Joe Gregoriobee86832011-02-22 10:00:19 -0500460 for name, enums in enum_params.iteritems():
461 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800462 # We need to handle the case of a repeated enum
463 # name differently, since we want to handle both
464 # arg='value' and arg=['value1', 'value2']
465 if (name in repeated_params and
466 not isinstance(kwargs[name], basestring)):
467 values = kwargs[name]
468 else:
469 values = [kwargs[name]]
470 for value in values:
471 if value not in enums:
472 raise TypeError(
473 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
474 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500475
ade@google.com850cf552010-08-20 23:24:56 +0100476 actual_query_params = {}
477 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400478 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500479 to_type = param_type.get(key, 'string')
480 # For repeated parameters we cast each member of the list.
481 if key in repeated_params and type(value) == type([]):
482 cast_value = [_cast(x, to_type) for x in value]
483 else:
484 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100485 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500486 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100487 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500488 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100489 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400490 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400491
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400492 if self._developerKey:
493 actual_query_params['key'] = self._developerKey
494
Joe Gregorioe08a1662011-12-07 09:48:22 -0500495 model = self._model
496 # If there is no schema for the response then presume a binary blob.
497 if 'response' not in methodDesc:
498 model = RawModel()
499
Joe Gregorio48d361f2010-08-18 13:19:21 -0400500 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500501 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400502 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400503
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400504 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400505 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
506
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500507 resumable = None
508 multipart_boundary = ''
509
Joe Gregorio922b78c2011-05-26 21:36:34 -0400510 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500511 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500512 if isinstance(media_filename, basestring):
513 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
514 if media_mime_type is None:
515 raise UnknownFileType(media_filename)
516 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
517 raise UnacceptableMimeTypeError(media_mime_type)
518 media_upload = MediaFileUpload(media_filename, media_mime_type)
519 elif isinstance(media_filename, MediaUpload):
520 media_upload = media_filename
521 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500522 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500523
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400524 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500525 if maxSize > 0 and media_upload.size() > maxSize:
526 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400527
528 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500529 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400530 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500531 if media_upload.resumable():
532 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400533
Joe Gregorio945be3e2012-01-27 17:01:06 -0500534 if media_upload.resumable():
535 # This is all we need to do for resumable, if the body exists it gets
536 # sent in the first request, otherwise an empty body is sent.
537 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400538 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500539 # A non-resumable upload
540 if body is None:
541 # This is a simple media upload
542 headers['content-type'] = media_upload.mimetype()
543 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500544 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500545 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500546 # This is a multipart/related upload.
547 msgRoot = MIMEMultipart('related')
548 # msgRoot should not write out it's own headers
549 setattr(msgRoot, '_write_headers', lambda self: None)
550
551 # attach the body as one part
552 msg = MIMENonMultipart(*headers['content-type'].split('/'))
553 msg.set_payload(body)
554 msgRoot.attach(msg)
555
556 # attach the media as the second part
557 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
558 msg['Content-Transfer-Encoding'] = 'binary'
559
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500560 payload = media_upload.getbytes(0, media_upload.size())
561 msg.set_payload(payload)
562 msgRoot.attach(msg)
563 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400564
Joe Gregorio945be3e2012-01-27 17:01:06 -0500565 multipart_boundary = msgRoot.get_boundary()
566 headers['content-type'] = ('multipart/related; '
567 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500568 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400569
Joe Gregorioe84c9442012-03-12 08:45:57 -0400570 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500571 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500572 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500573 url,
574 method=httpMethod,
575 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500576 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500577 methodId=methodId,
578 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400579
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500580 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
581 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500582 docs.append('Args:\n')
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400583
584 # Skip undocumented params and params common to all methods.
585 skip_parameters = rootDesc.get('parameters', {}).keys()
586 skip_parameters.append(STACK_QUERY_PARAMETERS)
587
Joe Gregorio48d361f2010-08-18 13:19:21 -0400588 for arg in argmap.iterkeys():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400589 if arg in skip_parameters:
Joe Gregorioca876e42011-02-22 19:39:42 -0500590 continue
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400591
Joe Gregorio61d7e962011-02-22 22:52:07 -0500592 repeated = ''
593 if arg in repeated_params:
594 repeated = ' (repeated)'
595 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400596 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500597 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500598 paramdesc = methodDesc['parameters'][argmap[arg]]
599 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500600 if '$ref' in paramdesc:
601 docs.append(
602 (' %s: object, %s%s%s\n The object takes the'
603 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
604 schema.prettyPrintByName(paramdesc['$ref'])))
605 else:
606 paramtype = paramdesc.get('type', 'string')
607 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
608 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500609 enum = paramdesc.get('enum', [])
610 enumDesc = paramdesc.get('enumDescriptions', [])
611 if enum and enumDesc:
612 docs.append(' Allowed values\n')
613 for (name, desc) in zip(enum, enumDesc):
614 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500615 if 'response' in methodDesc:
616 docs.append('\nReturns:\n An object of the form\n\n ')
617 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400618
619 setattr(method, '__doc__', ''.join(docs))
620 setattr(theclass, methodName, method)
621
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400622 def createNextMethod(theclass, methodName, methodDesc, rootDesc):
623 """Creates any _next methods for attaching to a Resource.
624
625 The _next methods allow for easy iteration through list() responses.
626
627 Args:
628 theclass: type, the class to attach methods to.
629 methodName: string, name of the method to use.
630 methodDesc: object, fragment of deserialized discovery document that
631 describes the method.
632 rootDesc: object, the entire deserialized discovery document.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400633 """
Joe Gregoriod92897c2011-07-07 11:44:56 -0400634 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400635 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400636
Joe Gregorio3c676f92011-07-25 10:38:14 -0400637 def methodNext(self, previous_request, previous_response):
638 """Retrieves the next page of results.
639
640 Args:
641 previous_request: The request for the previous page.
642 previous_response: The response from the request for the previous page.
643
644 Returns:
645 A request object that you can call 'execute()' on to request the next
646 page. Returns None if there are no more items in the collection.
647 """
648 # Retrieve nextPageToken from previous_response
649 # Use as pageToken in previous_request to create new request.
650
651 if 'nextPageToken' not in previous_response:
652 return None
653
654 request = copy.copy(previous_request)
655
656 pageToken = previous_response['nextPageToken']
657 parsed = list(urlparse.urlparse(request.uri))
658 q = parse_qsl(parsed[4])
659
660 # Find and remove old 'pageToken' value from URI
661 newq = [(key, value) for (key, value) in q if key != 'pageToken']
662 newq.append(('pageToken', pageToken))
663 parsed[4] = urllib.urlencode(newq)
664 uri = urlparse.urlunparse(parsed)
665
666 request.uri = uri
667
Joe Gregorioe84c9442012-03-12 08:45:57 -0400668 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400669
670 return request
671
672 setattr(theclass, methodName, methodNext)
673
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400674 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400675 if 'methods' in resourceDesc:
676 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400677 createMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400678
679 # Add in nested resources
680 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500681
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400682 def createResourceMethod(theclass, methodName, methodDesc, rootDesc):
683 """Create a method on the Resource to access a nested Resource.
684
685 Args:
686 theclass: type, the class to attach methods to.
687 methodName: string, name of the method to use.
688 methodDesc: object, fragment of deserialized discovery document that
689 describes the method.
690 rootDesc: object, the entire deserialized discovery document.
691 """
Joe Gregoriod92897c2011-07-07 11:44:56 -0400692 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400693
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500694 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400695 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500696 self._requestBuilder, self._developerKey,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400697 methodDesc, rootDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400698
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500699 setattr(methodResource, '__doc__', 'A collection resource.')
700 setattr(methodResource, '__is_resource__', True)
701 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400702
703 for methodName, methodDesc in resourceDesc['resources'].iteritems():
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400704 createResourceMethod(Resource, methodName, methodDesc, rootDesc)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400705
Joe Gregorio3c676f92011-07-25 10:38:14 -0400706 # Add _next() methods
707 # Look for response bodies in schema that contain nextPageToken, and methods
708 # that take a pageToken parameter.
709 if 'methods' in resourceDesc:
710 for methodName, methodDesc in resourceDesc['methods'].iteritems():
711 if 'response' in methodDesc:
712 responseSchema = methodDesc['response']
713 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500714 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400715 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
716 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400717 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
718 if hasNextPageToken and hasPageToken:
719 createNextMethod(Resource, methodName + '_next',
720 resourceDesc['methods'][methodName],
721 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400722
723 return Resource()