blob: 02ab99ed343972189cc96c83ff9c5a66d6f1b569 [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 Gregorio48d361f2010-08-18 13:19:21 -040058
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050059URITEMPLATE = re.compile('{[^}]*}')
60VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040061DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
62 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050063DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050064
65# Query parameters that work, but don't appear in discovery
Joe Gregorio06d852b2011-03-25 15:03:10 -040066STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
Joe Gregorio3eecaa92011-05-17 13:40:12 -040067 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040068
Joe Gregorio562b7312011-09-15 09:06:38 -040069RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040070 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
71 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
72 'pass', 'print', 'raise', 'return', 'try', 'while' ]
73
Joe Gregorio562b7312011-09-15 09:06:38 -040074
Joe Gregoriod92897c2011-07-07 11:44:56 -040075def _fix_method_name(name):
76 if name in RESERVED_WORDS:
77 return name + '_'
78 else:
79 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040080
Joe Gregorioa98733f2011-09-16 10:12:28 -040081
Joe Gregorio922b78c2011-05-26 21:36:34 -040082def _write_headers(self):
83 # Utility no-op method for multipart media handling
84 pass
85
86
Joe Gregorioa98733f2011-09-16 10:12:28 -040087def _add_query_parameter(url, name, value):
88 """Adds a query parameter to a url
89
90 Args:
91 url: string, url to add the query parameter to.
92 name: string, query parameter name.
93 value: string, query parameter value.
94
95 Returns:
96 Updated query parameter. Does not update the url if value is None.
97 """
98 if value is None:
99 return url
100 else:
101 parsed = list(urlparse.urlparse(url))
102 q = parse_qsl(parsed[4])
103 q.append((name, value))
104 parsed[4] = urllib.urlencode(q)
105 return urlparse.urlunparse(parsed)
106
107
Joe Gregorio48d361f2010-08-18 13:19:21 -0400108def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500109 """Converts key names into parameter names.
110
111 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -0400112 """
113 result = []
114 key = list(key)
115 if not key[0].isalpha():
116 result.append('x')
117 for c in key:
118 if c.isalnum():
119 result.append(c)
120 else:
121 result.append('_')
122
123 return ''.join(result)
124
125
Joe Gregorio01770a52012-02-24 11:11:10 -0500126def build(serviceName,
127 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500128 http=None,
129 discoveryServiceUrl=DISCOVERY_URI,
130 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500131 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500132 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500133 """Construct a Resource for interacting with an API.
134
135 Construct a Resource object for interacting with
136 an API. The serviceName and version are the
137 names from the Discovery service.
138
139 Args:
140 serviceName: string, name of the service
141 version: string, the version of the service
Joe Gregorio01770a52012-02-24 11:11:10 -0500142 http: httplib2.Http, An instance of httplib2.Http or something that acts
143 like it that HTTP requests will be made through.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500144 discoveryServiceUrl: string, a URI Template that points to
145 the location of the discovery service. It should have two
146 parameters {api} and {apiVersion} that when filled in
147 produce an absolute URI to the discovery document for
148 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500149 developerKey: string, key obtained
150 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500151 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500152 requestBuilder: apiclient.http.HttpRequest, encapsulator for
153 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500154
155 Returns:
156 A Resource object with methods for interacting with
157 the service.
158 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400159 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400160 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400161 'apiVersion': version
162 }
ade@google.com850cf552010-08-20 23:24:56 +0100163
Joe Gregorioc204b642010-09-21 12:01:23 -0400164 if http is None:
165 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400166
ade@google.com850cf552010-08-20 23:24:56 +0100167 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400168
Joe Gregorio66f57522011-11-30 11:00:00 -0500169 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
170 # variable that contains the network address of the client sending the
171 # request. If it exists then add that to the request for the discovery
172 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400173 if 'REMOTE_ADDR' in os.environ:
174 requested_url = _add_query_parameter(requested_url, 'userIp',
175 os.environ['REMOTE_ADDR'])
ade@google.com850cf552010-08-20 23:24:56 +0100176 logging.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400177
ade@google.com850cf552010-08-20 23:24:56 +0100178 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400179
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500180 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500181 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500182 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400183 if resp.status >= 400:
Joe Gregorio49396552011-03-08 10:39:00 -0500184 raise HttpError(resp, content, requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400185
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500186 try:
187 service = simplejson.loads(content)
188 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500189 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500190 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400191
Joe Gregorioa98733f2011-09-16 10:12:28 -0400192 filename = os.path.join(os.path.dirname(__file__), 'contrib',
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500193 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400194 try:
Joe Gregorioa98733f2011-09-16 10:12:28 -0400195 f = file(filename, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500196 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400197 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400198 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500199 future = None
200
201 return build_from_document(content, discoveryServiceUrl, future,
202 http, developerKey, model, requestBuilder)
203
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
215 Same as `build()`, but constructs the Resource object
216 from a discovery document that is it given, as opposed to
217 retrieving one over HTTP.
218
Joe Gregorio292b9b82011-01-12 11:36:11 -0500219 Args:
220 service: string, discovery document
221 base: string, base URI for all HTTP requests, usually the discovery URI
222 future: string, discovery document with future capabilities
223 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500224 http: httplib2.Http, An instance of httplib2.Http or something that acts
225 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500226 developerKey: string, Key for controlling API usage, generated
227 from the API Console.
228 model: Model class instance that serializes and
229 de-serializes requests and responses.
230 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500231
232 Returns:
233 A Resource object with methods for interacting with
234 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500235 """
236
237 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400238 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500239 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500240 future = simplejson.loads(future)
241 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500242 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400243 future = {}
244 auth_discovery = {}
Joe Gregorio2b781282011-12-08 12:00:25 -0500245 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400246
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500247 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500248 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500249 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500250 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400251 service, future, schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400252
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500253 def auth_method():
254 """Discovery information about the authentication the API uses."""
255 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400256
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500257 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400258
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500259 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400260
261
Joe Gregorio61d7e962011-02-22 22:52:07 -0500262def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500263 """Convert value to a string based on JSON Schema type.
264
265 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
266 JSON Schema.
267
268 Args:
269 value: any, the value to convert
270 schema_type: string, the type that value should be interpreted as
271
272 Returns:
273 A string representation of 'value' based on the schema_type.
274 """
275 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500276 if type(value) == type('') or type(value) == type(u''):
277 return value
278 else:
279 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500280 elif schema_type == 'integer':
281 return str(int(value))
282 elif schema_type == 'number':
283 return str(float(value))
284 elif schema_type == 'boolean':
285 return str(bool(value)).lower()
286 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500287 if type(value) == type('') or type(value) == type(u''):
288 return value
289 else:
290 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500291
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400292MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400293 "KB": 2 ** 10,
294 "MB": 2 ** 20,
295 "GB": 2 ** 30,
296 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400297 }
298
Joe Gregorioa98733f2011-09-16 10:12:28 -0400299
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400300def _media_size_to_long(maxSize):
301 """Convert a string media size, such as 10GB or 3TB into an integer."""
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400302 if len(maxSize) < 2:
303 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400304 units = maxSize[-2:].upper()
305 multiplier = MULTIPLIERS.get(units, 0)
306 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400307 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400308 else:
309 return int(maxSize)
310
Joe Gregoriobee86832011-02-22 10:00:19 -0500311
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500312def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400313 developerKey, resourceDesc, futureDesc, schema):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400314
315 class Resource(object):
316 """A class for interacting with a resource."""
317
318 def __init__(self):
319 self._http = http
320 self._baseUrl = baseUrl
321 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400322 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500323 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400324
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400325 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400326 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400327 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400328 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400329 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400330
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400331 mediaPathUrl = None
332 accept = []
333 maxSize = 0
334 if 'mediaUpload' in methodDesc:
335 mediaUpload = methodDesc['mediaUpload']
336 mediaPathUrl = mediaUpload['protocols']['simple']['path']
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500337 mediaResumablePathUrl = mediaUpload['protocols']['resumable']['path']
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400338 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400339 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400340
Joe Gregorioca876e42011-02-22 19:39:42 -0500341 if 'parameters' not in methodDesc:
342 methodDesc['parameters'] = {}
343 for name in STACK_QUERY_PARAMETERS:
344 methodDesc['parameters'][name] = {
345 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400346 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500347 }
348
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500349 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500350 methodDesc['parameters']['body'] = {
351 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500352 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500353 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500354 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500355 if 'request' in methodDesc:
356 methodDesc['parameters']['body'].update(methodDesc['request'])
357 else:
358 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500359 if 'mediaUpload' in methodDesc:
360 methodDesc['parameters']['media_body'] = {
361 'description': 'The filename of the media request body.',
362 'type': 'string',
363 'required': False,
364 }
365 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400366 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100367
Joe Gregorioca876e42011-02-22 19:39:42 -0500368 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100369 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500370 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100371 pattern_params = {} # Parameters that must match a regex
372 query_params = [] # Parameters that will be used in the query string
373 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500374 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500375 enum_params = {} # Allowable enumeration values for each parameter
376
377
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400378 if 'parameters' in methodDesc:
379 for arg, desc in methodDesc['parameters'].iteritems():
380 param = key2param(arg)
381 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400382
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400383 if desc.get('pattern', ''):
384 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500385 if desc.get('enum', ''):
386 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400387 if desc.get('required', False):
388 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500389 if desc.get('repeated', False):
390 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400391 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400392 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400393 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400394 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500395 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400396
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500397 for match in URITEMPLATE.finditer(pathUrl):
398 for namematch in VARNAME.finditer(match.group(0)):
399 name = key2param(namematch.group(0))
400 path_params[name] = name
401 if name in query_params:
402 query_params.remove(name)
403
Joe Gregorio48d361f2010-08-18 13:19:21 -0400404 def method(self, **kwargs):
405 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500406 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400407 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400408
ade@google.com850cf552010-08-20 23:24:56 +0100409 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400410 if name not in kwargs:
411 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400412
ade@google.com850cf552010-08-20 23:24:56 +0100413 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400414 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500415 if isinstance(kwargs[name], basestring):
416 pvalues = [kwargs[name]]
417 else:
418 pvalues = kwargs[name]
419 for pvalue in pvalues:
420 if re.match(regex, pvalue) is None:
421 raise TypeError(
422 'Parameter "%s" value "%s" does not match the pattern "%s"' %
423 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400424
Joe Gregoriobee86832011-02-22 10:00:19 -0500425 for name, enums in enum_params.iteritems():
426 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800427 # We need to handle the case of a repeated enum
428 # name differently, since we want to handle both
429 # arg='value' and arg=['value1', 'value2']
430 if (name in repeated_params and
431 not isinstance(kwargs[name], basestring)):
432 values = kwargs[name]
433 else:
434 values = [kwargs[name]]
435 for value in values:
436 if value not in enums:
437 raise TypeError(
438 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
439 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500440
ade@google.com850cf552010-08-20 23:24:56 +0100441 actual_query_params = {}
442 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400443 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500444 to_type = param_type.get(key, 'string')
445 # For repeated parameters we cast each member of the list.
446 if key in repeated_params and type(value) == type([]):
447 cast_value = [_cast(x, to_type) for x in value]
448 else:
449 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100450 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500451 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100452 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500453 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100454 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400455 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400456
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400457 if self._developerKey:
458 actual_query_params['key'] = self._developerKey
459
Joe Gregorioe08a1662011-12-07 09:48:22 -0500460 model = self._model
461 # If there is no schema for the response then presume a binary blob.
462 if 'response' not in methodDesc:
463 model = RawModel()
464
Joe Gregorio48d361f2010-08-18 13:19:21 -0400465 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500466 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400467 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400468
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400469 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400470 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
471
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500472 resumable = None
473 multipart_boundary = ''
474
Joe Gregorio922b78c2011-05-26 21:36:34 -0400475 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500476 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500477 if isinstance(media_filename, basestring):
478 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
479 if media_mime_type is None:
480 raise UnknownFileType(media_filename)
481 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
482 raise UnacceptableMimeTypeError(media_mime_type)
483 media_upload = MediaFileUpload(media_filename, media_mime_type)
484 elif isinstance(media_filename, MediaUpload):
485 media_upload = media_filename
486 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500487 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500488
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400489 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500490 if maxSize > 0 and media_upload.size() > maxSize:
491 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400492
493 # Use the media path uri for media uploads
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500494 if media_upload.resumable():
495 expanded_url = uritemplate.expand(mediaResumablePathUrl, params)
496 else:
497 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400498 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400499
Joe Gregorio945be3e2012-01-27 17:01:06 -0500500 if media_upload.resumable():
501 # This is all we need to do for resumable, if the body exists it gets
502 # sent in the first request, otherwise an empty body is sent.
503 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400504 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500505 # A non-resumable upload
506 if body is None:
507 # This is a simple media upload
508 headers['content-type'] = media_upload.mimetype()
509 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500510 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500511 # This is a multipart/related upload.
512 msgRoot = MIMEMultipart('related')
513 # msgRoot should not write out it's own headers
514 setattr(msgRoot, '_write_headers', lambda self: None)
515
516 # attach the body as one part
517 msg = MIMENonMultipart(*headers['content-type'].split('/'))
518 msg.set_payload(body)
519 msgRoot.attach(msg)
520
521 # attach the media as the second part
522 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
523 msg['Content-Transfer-Encoding'] = 'binary'
524
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500525 payload = media_upload.getbytes(0, media_upload.size())
526 msg.set_payload(payload)
527 msgRoot.attach(msg)
528 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400529
Joe Gregorio945be3e2012-01-27 17:01:06 -0500530 multipart_boundary = msgRoot.get_boundary()
531 headers['content-type'] = ('multipart/related; '
532 'boundary="%s"') % multipart_boundary
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400533
ade@google.com850cf552010-08-20 23:24:56 +0100534 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500535 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500536 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500537 url,
538 method=httpMethod,
539 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500540 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500541 methodId=methodId,
542 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400543
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500544 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
545 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500546 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400547 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500548 if arg in STACK_QUERY_PARAMETERS:
549 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500550 repeated = ''
551 if arg in repeated_params:
552 repeated = ' (repeated)'
553 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400554 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500555 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500556 paramdesc = methodDesc['parameters'][argmap[arg]]
557 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500558 if '$ref' in paramdesc:
559 docs.append(
560 (' %s: object, %s%s%s\n The object takes the'
561 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
562 schema.prettyPrintByName(paramdesc['$ref'])))
563 else:
564 paramtype = paramdesc.get('type', 'string')
565 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
566 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500567 enum = paramdesc.get('enum', [])
568 enumDesc = paramdesc.get('enumDescriptions', [])
569 if enum and enumDesc:
570 docs.append(' Allowed values\n')
571 for (name, desc) in zip(enum, enumDesc):
572 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500573 if 'response' in methodDesc:
574 docs.append('\nReturns:\n An object of the form\n\n ')
575 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400576
577 setattr(method, '__doc__', ''.join(docs))
578 setattr(theclass, methodName, method)
579
Joe Gregorio3c676f92011-07-25 10:38:14 -0400580 def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400581 """ This is a legacy method, as only Buzz and Moderator use the future.json
582 functionality for generating _next methods. It will be kept around as long
583 as those API versions are around, but no new APIs should depend upon it.
584 """
Joe Gregoriod92897c2011-07-07 11:44:56 -0400585 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400586 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400587
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500588 def methodNext(self, previous):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400589 """Retrieve the next page of results.
590
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400591 Takes a single argument, 'body', which is the results
592 from the last call, and returns the next set of items
593 in the collection.
594
Joe Gregorioa98733f2011-09-16 10:12:28 -0400595 Returns:
596 None if there are no more items in the collection.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400597 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500598 if futureDesc['type'] != 'uri':
599 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400600
601 try:
602 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500603 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400604 p = p[key]
605 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400606 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400607 return None
608
Joe Gregorioa98733f2011-09-16 10:12:28 -0400609 url = _add_query_parameter(url, 'key', self._developerKey)
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400610
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400611 headers = {}
612 headers, params, query, body = self._model.request(headers, {}, {}, None)
613
614 logging.info('URL being requested: %s' % url)
615 resp, content = self._http.request(url, method='GET', headers=headers)
616
Joe Gregorioabda96f2011-02-11 20:19:33 -0500617 return self._requestBuilder(self._http,
618 self._model.response,
619 url,
620 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500621 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500622 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400623
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500624 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400625
Joe Gregorio3c676f92011-07-25 10:38:14 -0400626 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
627 methodName = _fix_method_name(methodName)
628 methodId = methodDesc['id'] + '.next'
629
630 def methodNext(self, previous_request, previous_response):
631 """Retrieves the next page of results.
632
633 Args:
634 previous_request: The request for the previous page.
635 previous_response: The response from the request for the previous page.
636
637 Returns:
638 A request object that you can call 'execute()' on to request the next
639 page. Returns None if there are no more items in the collection.
640 """
641 # Retrieve nextPageToken from previous_response
642 # Use as pageToken in previous_request to create new request.
643
644 if 'nextPageToken' not in previous_response:
645 return None
646
647 request = copy.copy(previous_request)
648
649 pageToken = previous_response['nextPageToken']
650 parsed = list(urlparse.urlparse(request.uri))
651 q = parse_qsl(parsed[4])
652
653 # Find and remove old 'pageToken' value from URI
654 newq = [(key, value) for (key, value) in q if key != 'pageToken']
655 newq.append(('pageToken', pageToken))
656 parsed[4] = urllib.urlencode(newq)
657 uri = urlparse.urlunparse(parsed)
658
659 request.uri = uri
660
661 logging.info('URL being requested: %s' % uri)
662
663 return request
664
665 setattr(theclass, methodName, methodNext)
666
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400667 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400668 if 'methods' in resourceDesc:
669 for methodName, methodDesc in resourceDesc['methods'].iteritems():
670 if futureDesc:
671 future = futureDesc['methods'].get(methodName, {})
672 else:
673 future = None
674 createMethod(Resource, methodName, methodDesc, future)
675
676 # Add in nested resources
677 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500678
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500679 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400680 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400681
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500682 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400683 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500684 self._requestBuilder, self._developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400685 methodDesc, futureDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400686
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500687 setattr(methodResource, '__doc__', 'A collection resource.')
688 setattr(methodResource, '__is_resource__', True)
689 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400690
691 for methodName, methodDesc in resourceDesc['resources'].iteritems():
692 if futureDesc and 'resources' in futureDesc:
693 future = futureDesc['resources'].get(methodName, {})
694 else:
695 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500696 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400697
698 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500699 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400700 for methodName, methodDesc in futureDesc['methods'].iteritems():
701 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio3c676f92011-07-25 10:38:14 -0400702 createNextMethodFromFuture(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500703 resourceDesc['methods'][methodName],
704 methodDesc['next'])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400705 # Add _next() methods
706 # Look for response bodies in schema that contain nextPageToken, and methods
707 # that take a pageToken parameter.
708 if 'methods' in resourceDesc:
709 for methodName, methodDesc in resourceDesc['methods'].iteritems():
710 if 'response' in methodDesc:
711 responseSchema = methodDesc['response']
712 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500713 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400714 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
715 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400716 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
717 if hasNextPageToken and hasPageToken:
718 createNextMethod(Resource, methodName + '_next',
719 resourceDesc['methods'][methodName],
720 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400721
722 return Resource()