blob: 9344392692702dccbe38b7a17707834aea9e92ff [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.anyjson import simplejson
43from apiclient.errors import HttpError
44from apiclient.errors import InvalidJsonError
45from apiclient.errors import MediaUploadSizeError
46from apiclient.errors import UnacceptableMimeTypeError
47from apiclient.errors import UnknownApiNameOrVersion
48from apiclient.errors import UnknownLinkType
49from apiclient.http import HttpRequest
50from apiclient.http import MediaFileUpload
51from apiclient.http import MediaUpload
52from apiclient.model import JsonModel
53from apiclient.model import RawModel
54from apiclient.schema import Schemas
Joe Gregorio922b78c2011-05-26 21:36:34 -040055from email.mime.multipart import MIMEMultipart
56from email.mime.nonmultipart import MIMENonMultipart
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 Gregorioaf276d22010-12-09 14:26:58 -0500126def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500127 http=None,
128 discoveryServiceUrl=DISCOVERY_URI,
129 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500130 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500131 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500132 """Construct a Resource for interacting with an API.
133
134 Construct a Resource object for interacting with
135 an API. The serviceName and version are the
136 names from the Discovery service.
137
138 Args:
139 serviceName: string, name of the service
140 version: string, the version of the service
141 discoveryServiceUrl: string, a URI Template that points to
142 the location of the discovery service. It should have two
143 parameters {api} and {apiVersion} that when filled in
144 produce an absolute URI to the discovery document for
145 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500146 developerKey: string, key obtained
147 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500148 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500149 requestBuilder: apiclient.http.HttpRequest, encapsulator for
150 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500151
152 Returns:
153 A Resource object with methods for interacting with
154 the service.
155 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400156 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400157 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400158 'apiVersion': version
159 }
ade@google.com850cf552010-08-20 23:24:56 +0100160
Joe Gregorioc204b642010-09-21 12:01:23 -0400161 if http is None:
162 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400163
ade@google.com850cf552010-08-20 23:24:56 +0100164 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400165
Joe Gregorio66f57522011-11-30 11:00:00 -0500166 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
167 # variable that contains the network address of the client sending the
168 # request. If it exists then add that to the request for the discovery
169 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400170 if 'REMOTE_ADDR' in os.environ:
171 requested_url = _add_query_parameter(requested_url, 'userIp',
172 os.environ['REMOTE_ADDR'])
ade@google.com850cf552010-08-20 23:24:56 +0100173 logging.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400174
ade@google.com850cf552010-08-20 23:24:56 +0100175 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400176
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500177 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500178 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500179 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400180 if resp.status >= 400:
Joe Gregorio49396552011-03-08 10:39:00 -0500181 raise HttpError(resp, content, requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400182
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500183 try:
184 service = simplejson.loads(content)
185 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500186 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500187 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400188
Joe Gregorioa98733f2011-09-16 10:12:28 -0400189 filename = os.path.join(os.path.dirname(__file__), 'contrib',
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500190 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400191 try:
Joe Gregorioa98733f2011-09-16 10:12:28 -0400192 f = file(filename, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500193 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400194 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400195 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500196 future = None
197
198 return build_from_document(content, discoveryServiceUrl, future,
199 http, developerKey, model, requestBuilder)
200
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500201
Joe Gregorio292b9b82011-01-12 11:36:11 -0500202def build_from_document(
203 service,
204 base,
205 future=None,
206 http=None,
207 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500208 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500209 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500210 """Create a Resource for interacting with an API.
211
212 Same as `build()`, but constructs the Resource object
213 from a discovery document that is it given, as opposed to
214 retrieving one over HTTP.
215
Joe Gregorio292b9b82011-01-12 11:36:11 -0500216 Args:
217 service: string, discovery document
218 base: string, base URI for all HTTP requests, usually the discovery URI
219 future: string, discovery document with future capabilities
220 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500221 http: httplib2.Http, An instance of httplib2.Http or something that acts
222 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500223 developerKey: string, Key for controlling API usage, generated
224 from the API Console.
225 model: Model class instance that serializes and
226 de-serializes requests and responses.
227 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500228
229 Returns:
230 A Resource object with methods for interacting with
231 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500232 """
233
234 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400235 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500236 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500237 future = simplejson.loads(future)
238 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500239 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400240 future = {}
241 auth_discovery = {}
Joe Gregorio2b781282011-12-08 12:00:25 -0500242 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400243
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500244 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500245 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500246 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500247 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400248 service, future, schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400249
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500250 def auth_method():
251 """Discovery information about the authentication the API uses."""
252 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400253
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500254 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400255
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500256 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400257
258
Joe Gregorio61d7e962011-02-22 22:52:07 -0500259def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500260 """Convert value to a string based on JSON Schema type.
261
262 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
263 JSON Schema.
264
265 Args:
266 value: any, the value to convert
267 schema_type: string, the type that value should be interpreted as
268
269 Returns:
270 A string representation of 'value' based on the schema_type.
271 """
272 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500273 if type(value) == type('') or type(value) == type(u''):
274 return value
275 else:
276 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500277 elif schema_type == 'integer':
278 return str(int(value))
279 elif schema_type == 'number':
280 return str(float(value))
281 elif schema_type == 'boolean':
282 return str(bool(value)).lower()
283 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500284 if type(value) == type('') or type(value) == type(u''):
285 return value
286 else:
287 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500288
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400289MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400290 "KB": 2 ** 10,
291 "MB": 2 ** 20,
292 "GB": 2 ** 30,
293 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400294 }
295
Joe Gregorioa98733f2011-09-16 10:12:28 -0400296
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400297def _media_size_to_long(maxSize):
298 """Convert a string media size, such as 10GB or 3TB into an integer."""
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400299 if len(maxSize) < 2:
300 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400301 units = maxSize[-2:].upper()
302 multiplier = MULTIPLIERS.get(units, 0)
303 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400304 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400305 else:
306 return int(maxSize)
307
Joe Gregoriobee86832011-02-22 10:00:19 -0500308
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500309def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400310 developerKey, resourceDesc, futureDesc, schema):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400311
312 class Resource(object):
313 """A class for interacting with a resource."""
314
315 def __init__(self):
316 self._http = http
317 self._baseUrl = baseUrl
318 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400319 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500320 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400321
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400322 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400323 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400324 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400325 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400326 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400327
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400328 mediaPathUrl = None
329 accept = []
330 maxSize = 0
331 if 'mediaUpload' in methodDesc:
332 mediaUpload = methodDesc['mediaUpload']
333 mediaPathUrl = mediaUpload['protocols']['simple']['path']
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500334 mediaResumablePathUrl = mediaUpload['protocols']['resumable']['path']
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400335 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400336 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400337
Joe Gregorioca876e42011-02-22 19:39:42 -0500338 if 'parameters' not in methodDesc:
339 methodDesc['parameters'] = {}
340 for name in STACK_QUERY_PARAMETERS:
341 methodDesc['parameters'][name] = {
342 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400343 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500344 }
345
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500346 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500347 methodDesc['parameters']['body'] = {
348 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500349 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500350 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500351 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500352 if 'request' in methodDesc:
353 methodDesc['parameters']['body'].update(methodDesc['request'])
354 else:
355 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500356 if 'mediaUpload' in methodDesc:
357 methodDesc['parameters']['media_body'] = {
358 'description': 'The filename of the media request body.',
359 'type': 'string',
360 'required': False,
361 }
362 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400363 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100364
Joe Gregorioca876e42011-02-22 19:39:42 -0500365 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100366 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500367 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100368 pattern_params = {} # Parameters that must match a regex
369 query_params = [] # Parameters that will be used in the query string
370 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500371 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500372 enum_params = {} # Allowable enumeration values for each parameter
373
374
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400375 if 'parameters' in methodDesc:
376 for arg, desc in methodDesc['parameters'].iteritems():
377 param = key2param(arg)
378 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400379
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400380 if desc.get('pattern', ''):
381 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500382 if desc.get('enum', ''):
383 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400384 if desc.get('required', False):
385 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500386 if desc.get('repeated', False):
387 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400388 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400389 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400390 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400391 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500392 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400393
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500394 for match in URITEMPLATE.finditer(pathUrl):
395 for namematch in VARNAME.finditer(match.group(0)):
396 name = key2param(namematch.group(0))
397 path_params[name] = name
398 if name in query_params:
399 query_params.remove(name)
400
Joe Gregorio48d361f2010-08-18 13:19:21 -0400401 def method(self, **kwargs):
402 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500403 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400404 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400405
ade@google.com850cf552010-08-20 23:24:56 +0100406 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400407 if name not in kwargs:
408 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400409
ade@google.com850cf552010-08-20 23:24:56 +0100410 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400411 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500412 if isinstance(kwargs[name], basestring):
413 pvalues = [kwargs[name]]
414 else:
415 pvalues = kwargs[name]
416 for pvalue in pvalues:
417 if re.match(regex, pvalue) is None:
418 raise TypeError(
419 'Parameter "%s" value "%s" does not match the pattern "%s"' %
420 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400421
Joe Gregoriobee86832011-02-22 10:00:19 -0500422 for name, enums in enum_params.iteritems():
423 if name in kwargs:
424 if kwargs[name] not in enums:
425 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500426 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500427 (name, kwargs[name], str(enums)))
428
ade@google.com850cf552010-08-20 23:24:56 +0100429 actual_query_params = {}
430 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400431 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500432 to_type = param_type.get(key, 'string')
433 # For repeated parameters we cast each member of the list.
434 if key in repeated_params and type(value) == type([]):
435 cast_value = [_cast(x, to_type) for x in value]
436 else:
437 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100438 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500439 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100440 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500441 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100442 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400443 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400444
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400445 if self._developerKey:
446 actual_query_params['key'] = self._developerKey
447
Joe Gregorioe08a1662011-12-07 09:48:22 -0500448 model = self._model
449 # If there is no schema for the response then presume a binary blob.
450 if 'response' not in methodDesc:
451 model = RawModel()
452
Joe Gregorio48d361f2010-08-18 13:19:21 -0400453 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500454 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400455 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400456
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400457 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400458 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
459
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500460 resumable = None
461 multipart_boundary = ''
462
Joe Gregorio922b78c2011-05-26 21:36:34 -0400463 if media_filename:
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500464 # Convert a simple filename into a MediaUpload object.
465 if isinstance(media_filename, basestring):
466 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
467 if media_mime_type is None:
468 raise UnknownFileType(media_filename)
469 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
470 raise UnacceptableMimeTypeError(media_mime_type)
471 media_upload = MediaFileUpload(media_filename, media_mime_type)
472 elif isinstance(media_filename, MediaUpload):
473 media_upload = media_filename
474 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500475 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500476
477 if media_upload.resumable():
478 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400479
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400480 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500481 if maxSize > 0 and media_upload.size() > maxSize:
482 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400483
484 # Use the media path uri for media uploads
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500485 if media_upload.resumable():
486 expanded_url = uritemplate.expand(mediaResumablePathUrl, params)
487 else:
488 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400489 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400490
491 if body is None:
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500492 # This is a simple media upload
493 headers['content-type'] = media_upload.mimetype()
494 expanded_url = uritemplate.expand(mediaResumablePathUrl, params)
495 if not media_upload.resumable():
496 body = media_upload.getbytes(0, media_upload.size())
Joe Gregorio922b78c2011-05-26 21:36:34 -0400497 else:
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500498 # This is a multipart/related upload.
Joe Gregorio922b78c2011-05-26 21:36:34 -0400499 msgRoot = MIMEMultipart('related')
500 # msgRoot should not write out it's own headers
501 setattr(msgRoot, '_write_headers', lambda self: None)
502
503 # attach the body as one part
504 msg = MIMENonMultipart(*headers['content-type'].split('/'))
505 msg.set_payload(body)
506 msgRoot.attach(msg)
507
508 # attach the media as the second part
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500509 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
Joe Gregorio922b78c2011-05-26 21:36:34 -0400510 msg['Content-Transfer-Encoding'] = 'binary'
511
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500512 if media_upload.resumable():
513 # This is a multipart resumable upload, where a multipart payload
514 # looks like this:
515 #
516 # --===============1678050750164843052==
517 # Content-Type: application/json
518 # MIME-Version: 1.0
519 #
520 # {'foo': 'bar'}
521 # --===============1678050750164843052==
522 # Content-Type: image/png
523 # MIME-Version: 1.0
524 # Content-Transfer-Encoding: binary
525 #
526 # <BINARY STUFF>
527 # --===============1678050750164843052==--
528 #
529 # In the case of resumable multipart media uploads, the <BINARY
530 # STUFF> is large and will be spread across multiple PUTs. What we
531 # do here is compose the multipart message with a random payload in
532 # place of <BINARY STUFF> and then split the resulting content into
533 # two pieces, text before <BINARY STUFF> and text after <BINARY
534 # STUFF>. The text after <BINARY STUFF> is the multipart boundary.
535 # In apiclient.http the HttpRequest will send the text before
536 # <BINARY STUFF>, then send the actual binary media in chunks, and
537 # then will send the multipart delimeter.
Joe Gregorio922b78c2011-05-26 21:36:34 -0400538
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500539 payload = hex(random.getrandbits(300))
540 msg.set_payload(payload)
541 msgRoot.attach(msg)
542 body = msgRoot.as_string()
543 body, _ = body.split(payload)
544 resumable = media_upload
545 else:
546 payload = media_upload.getbytes(0, media_upload.size())
547 msg.set_payload(payload)
548 msgRoot.attach(msg)
549 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400550
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500551 multipart_boundary = msgRoot.get_boundary()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400552 headers['content-type'] = ('multipart/related; '
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500553 'boundary="%s"') % multipart_boundary
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400554
ade@google.com850cf552010-08-20 23:24:56 +0100555 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500556 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500557 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500558 url,
559 method=httpMethod,
560 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500561 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500562 methodId=methodId,
563 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400564
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500565 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
566 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500567 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400568 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500569 if arg in STACK_QUERY_PARAMETERS:
570 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500571 repeated = ''
572 if arg in repeated_params:
573 repeated = ' (repeated)'
574 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400575 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500576 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500577 paramdesc = methodDesc['parameters'][argmap[arg]]
578 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500579 if '$ref' in paramdesc:
580 docs.append(
581 (' %s: object, %s%s%s\n The object takes the'
582 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
583 schema.prettyPrintByName(paramdesc['$ref'])))
584 else:
585 paramtype = paramdesc.get('type', 'string')
586 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
587 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500588 enum = paramdesc.get('enum', [])
589 enumDesc = paramdesc.get('enumDescriptions', [])
590 if enum and enumDesc:
591 docs.append(' Allowed values\n')
592 for (name, desc) in zip(enum, enumDesc):
593 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500594 if 'response' in methodDesc:
595 docs.append('\nReturns:\n An object of the form\n\n ')
596 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400597
598 setattr(method, '__doc__', ''.join(docs))
599 setattr(theclass, methodName, method)
600
Joe Gregorio3c676f92011-07-25 10:38:14 -0400601 def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400602 """ This is a legacy method, as only Buzz and Moderator use the future.json
603 functionality for generating _next methods. It will be kept around as long
604 as those API versions are around, but no new APIs should depend upon it.
605 """
Joe Gregoriod92897c2011-07-07 11:44:56 -0400606 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400607 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400608
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500609 def methodNext(self, previous):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400610 """Retrieve the next page of results.
611
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400612 Takes a single argument, 'body', which is the results
613 from the last call, and returns the next set of items
614 in the collection.
615
Joe Gregorioa98733f2011-09-16 10:12:28 -0400616 Returns:
617 None if there are no more items in the collection.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400618 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500619 if futureDesc['type'] != 'uri':
620 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400621
622 try:
623 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500624 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400625 p = p[key]
626 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400627 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400628 return None
629
Joe Gregorioa98733f2011-09-16 10:12:28 -0400630 url = _add_query_parameter(url, 'key', self._developerKey)
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400631
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400632 headers = {}
633 headers, params, query, body = self._model.request(headers, {}, {}, None)
634
635 logging.info('URL being requested: %s' % url)
636 resp, content = self._http.request(url, method='GET', headers=headers)
637
Joe Gregorioabda96f2011-02-11 20:19:33 -0500638 return self._requestBuilder(self._http,
639 self._model.response,
640 url,
641 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500642 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500643 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400644
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500645 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400646
Joe Gregorio3c676f92011-07-25 10:38:14 -0400647 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
648 methodName = _fix_method_name(methodName)
649 methodId = methodDesc['id'] + '.next'
650
651 def methodNext(self, previous_request, previous_response):
652 """Retrieves the next page of results.
653
654 Args:
655 previous_request: The request for the previous page.
656 previous_response: The response from the request for the previous page.
657
658 Returns:
659 A request object that you can call 'execute()' on to request the next
660 page. Returns None if there are no more items in the collection.
661 """
662 # Retrieve nextPageToken from previous_response
663 # Use as pageToken in previous_request to create new request.
664
665 if 'nextPageToken' not in previous_response:
666 return None
667
668 request = copy.copy(previous_request)
669
670 pageToken = previous_response['nextPageToken']
671 parsed = list(urlparse.urlparse(request.uri))
672 q = parse_qsl(parsed[4])
673
674 # Find and remove old 'pageToken' value from URI
675 newq = [(key, value) for (key, value) in q if key != 'pageToken']
676 newq.append(('pageToken', pageToken))
677 parsed[4] = urllib.urlencode(newq)
678 uri = urlparse.urlunparse(parsed)
679
680 request.uri = uri
681
682 logging.info('URL being requested: %s' % uri)
683
684 return request
685
686 setattr(theclass, methodName, methodNext)
687
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400688 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400689 if 'methods' in resourceDesc:
690 for methodName, methodDesc in resourceDesc['methods'].iteritems():
691 if futureDesc:
692 future = futureDesc['methods'].get(methodName, {})
693 else:
694 future = None
695 createMethod(Resource, methodName, methodDesc, future)
696
697 # Add in nested resources
698 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500699
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500700 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400701 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400702
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500703 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400704 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500705 self._requestBuilder, self._developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400706 methodDesc, futureDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400707
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500708 setattr(methodResource, '__doc__', 'A collection resource.')
709 setattr(methodResource, '__is_resource__', True)
710 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400711
712 for methodName, methodDesc in resourceDesc['resources'].iteritems():
713 if futureDesc and 'resources' in futureDesc:
714 future = futureDesc['resources'].get(methodName, {})
715 else:
716 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500717 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400718
719 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500720 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400721 for methodName, methodDesc in futureDesc['methods'].iteritems():
722 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio3c676f92011-07-25 10:38:14 -0400723 createNextMethodFromFuture(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500724 resourceDesc['methods'][methodName],
725 methodDesc['next'])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400726 # Add _next() methods
727 # Look for response bodies in schema that contain nextPageToken, and methods
728 # that take a pageToken parameter.
729 if 'methods' in resourceDesc:
730 for methodName, methodDesc in resourceDesc['methods'].iteritems():
731 if 'response' in methodDesc:
732 responseSchema = methodDesc['response']
733 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500734 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400735 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
736 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400737 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
738 if hasNextPageToken and hasPageToken:
739 createNextMethod(Resource, methodName + '_next',
740 resourceDesc['methods'][methodName],
741 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400742
743 return Resource()