blob: 0c44516d68672c82cafb9f51e1213472eb8bc211 [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
66# Query parameters that work, but don't appear in discovery
Joe Gregorio06d852b2011-03-25 15:03:10 -040067STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
Joe Gregorio3eecaa92011-05-17 13:40:12 -040068 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040069
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):
77 if name in RESERVED_WORDS:
78 return name + '_'
79 else:
80 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040081
Joe Gregorioa98733f2011-09-16 10:12:28 -040082
Joe Gregorio922b78c2011-05-26 21:36:34 -040083def _write_headers(self):
84 # Utility no-op method for multipart media handling
85 pass
86
87
Joe Gregorioa98733f2011-09-16 10:12:28 -040088def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050089 """Adds a query parameter to a url.
90
91 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -040092
93 Args:
94 url: string, url to add the query parameter to.
95 name: string, query parameter name.
96 value: string, query parameter value.
97
98 Returns:
99 Updated query parameter. Does not update the url if value is None.
100 """
101 if value is None:
102 return url
103 else:
104 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500105 q = dict(parse_qsl(parsed[4]))
106 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400107 parsed[4] = urllib.urlencode(q)
108 return urlparse.urlunparse(parsed)
109
110
Joe Gregorio48d361f2010-08-18 13:19:21 -0400111def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500112 """Converts key names into parameter names.
113
114 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -0400115 """
116 result = []
117 key = list(key)
118 if not key[0].isalpha():
119 result.append('x')
120 for c in key:
121 if c.isalnum():
122 result.append(c)
123 else:
124 result.append('_')
125
126 return ''.join(result)
127
128
Joe Gregorio01770a52012-02-24 11:11:10 -0500129def build(serviceName,
130 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500131 http=None,
132 discoveryServiceUrl=DISCOVERY_URI,
133 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500134 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500135 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500136 """Construct a Resource for interacting with an API.
137
138 Construct a Resource object for interacting with
139 an API. The serviceName and version are the
140 names from the Discovery service.
141
142 Args:
143 serviceName: string, name of the service
144 version: string, the version of the service
Joe Gregorio01770a52012-02-24 11:11:10 -0500145 http: httplib2.Http, An instance of httplib2.Http or something that acts
146 like it that HTTP requests will be made through.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500147 discoveryServiceUrl: string, a URI Template that points to
148 the location of the discovery service. It should have two
149 parameters {api} and {apiVersion} that when filled in
150 produce an absolute URI to the discovery document for
151 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500152 developerKey: string, key obtained
153 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500154 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500155 requestBuilder: apiclient.http.HttpRequest, encapsulator for
156 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500157
158 Returns:
159 A Resource object with methods for interacting with
160 the service.
161 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400162 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400163 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400164 'apiVersion': version
165 }
ade@google.com850cf552010-08-20 23:24:56 +0100166
Joe Gregorioc204b642010-09-21 12:01:23 -0400167 if http is None:
168 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400169
ade@google.com850cf552010-08-20 23:24:56 +0100170 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400171
Joe Gregorio66f57522011-11-30 11:00:00 -0500172 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
173 # variable that contains the network address of the client sending the
174 # request. If it exists then add that to the request for the discovery
175 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400176 if 'REMOTE_ADDR' in os.environ:
177 requested_url = _add_query_parameter(requested_url, 'userIp',
178 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400179 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400180
ade@google.com850cf552010-08-20 23:24:56 +0100181 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400182
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500183 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500184 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500185 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400186 if resp.status >= 400:
Joe Gregorio49396552011-03-08 10:39:00 -0500187 raise HttpError(resp, content, requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400188
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500189 try:
190 service = simplejson.loads(content)
191 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400192 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500193 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400194
Joe Gregorioa98733f2011-09-16 10:12:28 -0400195 filename = os.path.join(os.path.dirname(__file__), 'contrib',
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500196 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400197 try:
Joe Gregorioa98733f2011-09-16 10:12:28 -0400198 f = file(filename, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500199 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400200 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400201 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500202 future = None
203
204 return build_from_document(content, discoveryServiceUrl, future,
205 http, developerKey, model, requestBuilder)
206
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500207
Joe Gregorio292b9b82011-01-12 11:36:11 -0500208def build_from_document(
209 service,
210 base,
211 future=None,
212 http=None,
213 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500214 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500215 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500216 """Create a Resource for interacting with an API.
217
218 Same as `build()`, but constructs the Resource object
219 from a discovery document that is it given, as opposed to
220 retrieving one over HTTP.
221
Joe Gregorio292b9b82011-01-12 11:36:11 -0500222 Args:
223 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
226 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500227 http: httplib2.Http, An instance of httplib2.Http or something that acts
228 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500229 developerKey: string, Key for controlling API usage, generated
230 from the API Console.
231 model: Model class instance that serializes and
232 de-serializes requests and responses.
233 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500234
235 Returns:
236 A Resource object with methods for interacting with
237 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500238 """
239
240 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400241 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500242 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500243 future = simplejson.loads(future)
244 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500245 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400246 future = {}
247 auth_discovery = {}
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 Gregorio7a6df3a2011-01-31 21:55:21 -0500253 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400254 service, future, schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400255
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500256 def auth_method():
257 """Discovery information about the authentication the API uses."""
258 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400259
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500260 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400261
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500262 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400263
264
Joe Gregorio61d7e962011-02-22 22:52:07 -0500265def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500266 """Convert value to a string based on JSON Schema type.
267
268 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
269 JSON Schema.
270
271 Args:
272 value: any, the value to convert
273 schema_type: string, the type that value should be interpreted as
274
275 Returns:
276 A string representation of 'value' based on the schema_type.
277 """
278 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500279 if type(value) == type('') or type(value) == type(u''):
280 return value
281 else:
282 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500283 elif schema_type == 'integer':
284 return str(int(value))
285 elif schema_type == 'number':
286 return str(float(value))
287 elif schema_type == 'boolean':
288 return str(bool(value)).lower()
289 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500290 if type(value) == type('') or type(value) == type(u''):
291 return value
292 else:
293 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500294
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400295MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400296 "KB": 2 ** 10,
297 "MB": 2 ** 20,
298 "GB": 2 ** 30,
299 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400300 }
301
Joe Gregorioa98733f2011-09-16 10:12:28 -0400302
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400303def _media_size_to_long(maxSize):
304 """Convert a string media size, such as 10GB or 3TB into an integer."""
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400305 if len(maxSize) < 2:
306 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400307 units = maxSize[-2:].upper()
308 multiplier = MULTIPLIERS.get(units, 0)
309 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400310 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400311 else:
312 return int(maxSize)
313
Joe Gregoriobee86832011-02-22 10:00:19 -0500314
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500315def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400316 developerKey, resourceDesc, futureDesc, schema):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400317
318 class Resource(object):
319 """A class for interacting with a resource."""
320
321 def __init__(self):
322 self._http = http
323 self._baseUrl = baseUrl
324 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400325 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500326 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400327
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400328 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400329 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400330 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400331 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400332 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400333
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400334 mediaPathUrl = None
335 accept = []
336 maxSize = 0
337 if 'mediaUpload' in methodDesc:
338 mediaUpload = methodDesc['mediaUpload']
Joe Gregoriode860442012-03-02 15:55:52 -0500339 # TODO(jcgregorio) Use URLs from discovery once it is updated.
340 parsed = list(urlparse.urlparse(baseUrl))
341 basePath = parsed[2]
342 mediaPathUrl = '/upload' + basePath + pathUrl
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400343 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400344 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400345
Joe Gregorioca876e42011-02-22 19:39:42 -0500346 if 'parameters' not in methodDesc:
347 methodDesc['parameters'] = {}
348 for name in STACK_QUERY_PARAMETERS:
349 methodDesc['parameters'][name] = {
350 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400351 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500352 }
353
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500354 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500355 methodDesc['parameters']['body'] = {
356 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500357 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500358 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500359 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500360 if 'request' in methodDesc:
361 methodDesc['parameters']['body'].update(methodDesc['request'])
362 else:
363 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500364 if 'mediaUpload' in methodDesc:
365 methodDesc['parameters']['media_body'] = {
366 'description': 'The filename of the media request body.',
367 'type': 'string',
368 'required': False,
369 }
370 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400371 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100372
Joe Gregorioca876e42011-02-22 19:39:42 -0500373 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100374 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500375 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100376 pattern_params = {} # Parameters that must match a regex
377 query_params = [] # Parameters that will be used in the query string
378 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500379 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500380 enum_params = {} # Allowable enumeration values for each parameter
381
382
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400383 if 'parameters' in methodDesc:
384 for arg, desc in methodDesc['parameters'].iteritems():
385 param = key2param(arg)
386 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400387
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400388 if desc.get('pattern', ''):
389 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500390 if desc.get('enum', ''):
391 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400392 if desc.get('required', False):
393 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500394 if desc.get('repeated', False):
395 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400396 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400397 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400398 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400399 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500400 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400401
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500402 for match in URITEMPLATE.finditer(pathUrl):
403 for namematch in VARNAME.finditer(match.group(0)):
404 name = key2param(namematch.group(0))
405 path_params[name] = name
406 if name in query_params:
407 query_params.remove(name)
408
Joe Gregorio48d361f2010-08-18 13:19:21 -0400409 def method(self, **kwargs):
410 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500411 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400412 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400413
ade@google.com850cf552010-08-20 23:24:56 +0100414 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400415 if name not in kwargs:
416 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400417
ade@google.com850cf552010-08-20 23:24:56 +0100418 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400419 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500420 if isinstance(kwargs[name], basestring):
421 pvalues = [kwargs[name]]
422 else:
423 pvalues = kwargs[name]
424 for pvalue in pvalues:
425 if re.match(regex, pvalue) is None:
426 raise TypeError(
427 'Parameter "%s" value "%s" does not match the pattern "%s"' %
428 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400429
Joe Gregoriobee86832011-02-22 10:00:19 -0500430 for name, enums in enum_params.iteritems():
431 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800432 # We need to handle the case of a repeated enum
433 # name differently, since we want to handle both
434 # arg='value' and arg=['value1', 'value2']
435 if (name in repeated_params and
436 not isinstance(kwargs[name], basestring)):
437 values = kwargs[name]
438 else:
439 values = [kwargs[name]]
440 for value in values:
441 if value not in enums:
442 raise TypeError(
443 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
444 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500445
ade@google.com850cf552010-08-20 23:24:56 +0100446 actual_query_params = {}
447 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400448 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500449 to_type = param_type.get(key, 'string')
450 # For repeated parameters we cast each member of the list.
451 if key in repeated_params and type(value) == type([]):
452 cast_value = [_cast(x, to_type) for x in value]
453 else:
454 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100455 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500456 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100457 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500458 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100459 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400460 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400461
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400462 if self._developerKey:
463 actual_query_params['key'] = self._developerKey
464
Joe Gregorioe08a1662011-12-07 09:48:22 -0500465 model = self._model
466 # If there is no schema for the response then presume a binary blob.
467 if 'response' not in methodDesc:
468 model = RawModel()
469
Joe Gregorio48d361f2010-08-18 13:19:21 -0400470 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500471 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400472 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400473
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400474 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400475 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
476
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500477 resumable = None
478 multipart_boundary = ''
479
Joe Gregorio922b78c2011-05-26 21:36:34 -0400480 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500481 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500482 if isinstance(media_filename, basestring):
483 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
484 if media_mime_type is None:
485 raise UnknownFileType(media_filename)
486 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
487 raise UnacceptableMimeTypeError(media_mime_type)
488 media_upload = MediaFileUpload(media_filename, media_mime_type)
489 elif isinstance(media_filename, MediaUpload):
490 media_upload = media_filename
491 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500492 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500493
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400494 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500495 if maxSize > 0 and media_upload.size() > maxSize:
496 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400497
498 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500499 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400500 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500501 if media_upload.resumable():
502 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400503
Joe Gregorio945be3e2012-01-27 17:01:06 -0500504 if media_upload.resumable():
505 # This is all we need to do for resumable, if the body exists it gets
506 # sent in the first request, otherwise an empty body is sent.
507 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400508 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500509 # A non-resumable upload
510 if body is None:
511 # This is a simple media upload
512 headers['content-type'] = media_upload.mimetype()
513 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500514 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500515 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500516 # This is a multipart/related upload.
517 msgRoot = MIMEMultipart('related')
518 # msgRoot should not write out it's own headers
519 setattr(msgRoot, '_write_headers', lambda self: None)
520
521 # attach the body as one part
522 msg = MIMENonMultipart(*headers['content-type'].split('/'))
523 msg.set_payload(body)
524 msgRoot.attach(msg)
525
526 # attach the media as the second part
527 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
528 msg['Content-Transfer-Encoding'] = 'binary'
529
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500530 payload = media_upload.getbytes(0, media_upload.size())
531 msg.set_payload(payload)
532 msgRoot.attach(msg)
533 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400534
Joe Gregorio945be3e2012-01-27 17:01:06 -0500535 multipart_boundary = msgRoot.get_boundary()
536 headers['content-type'] = ('multipart/related; '
537 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500538 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400539
Joe Gregorioe84c9442012-03-12 08:45:57 -0400540 logger.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500541 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500542 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500543 url,
544 method=httpMethod,
545 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500546 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500547 methodId=methodId,
548 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400549
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500550 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
551 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500552 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400553 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500554 if arg in STACK_QUERY_PARAMETERS:
555 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500556 repeated = ''
557 if arg in repeated_params:
558 repeated = ' (repeated)'
559 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400560 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500561 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500562 paramdesc = methodDesc['parameters'][argmap[arg]]
563 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500564 if '$ref' in paramdesc:
565 docs.append(
566 (' %s: object, %s%s%s\n The object takes the'
567 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
568 schema.prettyPrintByName(paramdesc['$ref'])))
569 else:
570 paramtype = paramdesc.get('type', 'string')
571 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
572 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500573 enum = paramdesc.get('enum', [])
574 enumDesc = paramdesc.get('enumDescriptions', [])
575 if enum and enumDesc:
576 docs.append(' Allowed values\n')
577 for (name, desc) in zip(enum, enumDesc):
578 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500579 if 'response' in methodDesc:
580 docs.append('\nReturns:\n An object of the form\n\n ')
581 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400582
583 setattr(method, '__doc__', ''.join(docs))
584 setattr(theclass, methodName, method)
585
Joe Gregorio3c676f92011-07-25 10:38:14 -0400586 def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400587 """ This is a legacy method, as only Buzz and Moderator use the future.json
588 functionality for generating _next methods. It will be kept around as long
589 as those API versions are around, but no new APIs should depend upon it.
590 """
Joe Gregoriod92897c2011-07-07 11:44:56 -0400591 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400592 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400593
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500594 def methodNext(self, previous):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400595 """Retrieve the next page of results.
596
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400597 Takes a single argument, 'body', which is the results
598 from the last call, and returns the next set of items
599 in the collection.
600
Joe Gregorioa98733f2011-09-16 10:12:28 -0400601 Returns:
602 None if there are no more items in the collection.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400603 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500604 if futureDesc['type'] != 'uri':
605 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400606
607 try:
608 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500609 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400610 p = p[key]
611 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400612 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400613 return None
614
Joe Gregorioa98733f2011-09-16 10:12:28 -0400615 url = _add_query_parameter(url, 'key', self._developerKey)
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400616
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400617 headers = {}
618 headers, params, query, body = self._model.request(headers, {}, {}, None)
619
Joe Gregorioe84c9442012-03-12 08:45:57 -0400620 logger.info('URL being requested: %s' % url)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400621 resp, content = self._http.request(url, method='GET', headers=headers)
622
Joe Gregorioabda96f2011-02-11 20:19:33 -0500623 return self._requestBuilder(self._http,
624 self._model.response,
625 url,
626 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500627 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500628 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400629
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500630 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400631
Joe Gregorio3c676f92011-07-25 10:38:14 -0400632 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
633 methodName = _fix_method_name(methodName)
634 methodId = methodDesc['id'] + '.next'
635
636 def methodNext(self, previous_request, previous_response):
637 """Retrieves the next page of results.
638
639 Args:
640 previous_request: The request for the previous page.
641 previous_response: The response from the request for the previous page.
642
643 Returns:
644 A request object that you can call 'execute()' on to request the next
645 page. Returns None if there are no more items in the collection.
646 """
647 # Retrieve nextPageToken from previous_response
648 # Use as pageToken in previous_request to create new request.
649
650 if 'nextPageToken' not in previous_response:
651 return None
652
653 request = copy.copy(previous_request)
654
655 pageToken = previous_response['nextPageToken']
656 parsed = list(urlparse.urlparse(request.uri))
657 q = parse_qsl(parsed[4])
658
659 # Find and remove old 'pageToken' value from URI
660 newq = [(key, value) for (key, value) in q if key != 'pageToken']
661 newq.append(('pageToken', pageToken))
662 parsed[4] = urllib.urlencode(newq)
663 uri = urlparse.urlunparse(parsed)
664
665 request.uri = uri
666
Joe Gregorioe84c9442012-03-12 08:45:57 -0400667 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400668
669 return request
670
671 setattr(theclass, methodName, methodNext)
672
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400673 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400674 if 'methods' in resourceDesc:
675 for methodName, methodDesc in resourceDesc['methods'].iteritems():
676 if futureDesc:
677 future = futureDesc['methods'].get(methodName, {})
678 else:
679 future = None
680 createMethod(Resource, methodName, methodDesc, future)
681
682 # Add in nested resources
683 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500684
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500685 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400686 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400687
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500688 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400689 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500690 self._requestBuilder, self._developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400691 methodDesc, futureDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400692
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500693 setattr(methodResource, '__doc__', 'A collection resource.')
694 setattr(methodResource, '__is_resource__', True)
695 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400696
697 for methodName, methodDesc in resourceDesc['resources'].iteritems():
698 if futureDesc and 'resources' in futureDesc:
699 future = futureDesc['resources'].get(methodName, {})
700 else:
701 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500702 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400703
704 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500705 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400706 for methodName, methodDesc in futureDesc['methods'].iteritems():
707 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio3c676f92011-07-25 10:38:14 -0400708 createNextMethodFromFuture(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500709 resourceDesc['methods'][methodName],
710 methodDesc['next'])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400711 # Add _next() methods
712 # Look for response bodies in schema that contain nextPageToken, and methods
713 # that take a pageToken parameter.
714 if 'methods' in resourceDesc:
715 for methodName, methodDesc in resourceDesc['methods'].iteritems():
716 if 'response' in methodDesc:
717 responseSchema = methodDesc['response']
718 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500719 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400720 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
721 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400722 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
723 if hasNextPageToken and hasPageToken:
724 createNextMethod(Resource, methodName + '_next',
725 resourceDesc['methods'][methodName],
726 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400727
728 return Resource()