blob: 5f3c2679566476df89c9837dc0a1bb002b4ad752 [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 Gregorio48d361f2010-08-18 13:19:21 -040029import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040030import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040031import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040032import urlparse
Joe Gregoriofdf7c802011-06-30 12:33:38 -040033import mimeparse
Joe Gregorio922b78c2011-05-26 21:36:34 -040034import mimetypes
35
ade@google.comc5eb46f2010-09-27 23:35:39 +010036try:
37 from urlparse import parse_qsl
38except ImportError:
39 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050040
Joe Gregorio034e7002010-12-15 08:45:03 -050041from anyjson import simplejson
Joe Gregorio922b78c2011-05-26 21:36:34 -040042from email.mime.multipart import MIMEMultipart
43from email.mime.nonmultipart import MIMENonMultipart
Joe Gregorioc0e0fe92011-03-04 16:16:55 -050044from errors import HttpError
Joe Gregorio49396552011-03-08 10:39:00 -050045from errors import InvalidJsonError
Joe Gregoriofdf7c802011-06-30 12:33:38 -040046from errors import MediaUploadSizeError
47from errors import UnacceptableMimeTypeError
Joe Gregorio922b78c2011-05-26 21:36:34 -040048from errors import UnknownLinkType
49from http import HttpRequest
50from model import JsonModel
Joe Gregorio48d361f2010-08-18 13:19:21 -040051
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050052URITEMPLATE = re.compile('{[^}]*}')
53VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040054DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
55 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050056DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050057
58# Query parameters that work, but don't appear in discovery
Joe Gregorio06d852b2011-03-25 15:03:10 -040059STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
Joe Gregorio3eecaa92011-05-17 13:40:12 -040060 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040061
Joe Gregoriod92897c2011-07-07 11:44:56 -040062RESERVED_WORDS = [ 'and', 'assert', 'break', 'class', 'continue', 'def', 'del',
63 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
64 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
65 'pass', 'print', 'raise', 'return', 'try', 'while' ]
66
67def _fix_method_name(name):
68 if name in RESERVED_WORDS:
69 return name + '_'
70 else:
71 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040072
Joe Gregorio922b78c2011-05-26 21:36:34 -040073def _write_headers(self):
74 # Utility no-op method for multipart media handling
75 pass
76
77
Joe Gregorio48d361f2010-08-18 13:19:21 -040078def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050079 """Converts key names into parameter names.
80
81 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040082 """
83 result = []
84 key = list(key)
85 if not key[0].isalpha():
86 result.append('x')
87 for c in key:
88 if c.isalnum():
89 result.append(c)
90 else:
91 result.append('_')
92
93 return ''.join(result)
94
95
Joe Gregorioaf276d22010-12-09 14:26:58 -050096def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050097 http=None,
98 discoveryServiceUrl=DISCOVERY_URI,
99 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500100 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500101 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500102 """Construct a Resource for interacting with an API.
103
104 Construct a Resource object for interacting with
105 an API. The serviceName and version are the
106 names from the Discovery service.
107
108 Args:
109 serviceName: string, name of the service
110 version: string, the version of the service
111 discoveryServiceUrl: string, a URI Template that points to
112 the location of the discovery service. It should have two
113 parameters {api} and {apiVersion} that when filled in
114 produce an absolute URI to the discovery document for
115 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500116 developerKey: string, key obtained
117 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500118 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500119 requestBuilder: apiclient.http.HttpRequest, encapsulator for
120 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500121
122 Returns:
123 A Resource object with methods for interacting with
124 the service.
125 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400126 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400127 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400128 'apiVersion': version
129 }
ade@google.com850cf552010-08-20 23:24:56 +0100130
Joe Gregorioc204b642010-09-21 12:01:23 -0400131 if http is None:
132 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100133 requested_url = uritemplate.expand(discoveryServiceUrl, params)
134 logging.info('URL being requested: %s' % requested_url)
135 resp, content = http.request(requested_url)
Joe Gregorio49396552011-03-08 10:39:00 -0500136 if resp.status > 400:
137 raise HttpError(resp, content, requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500138 try:
139 service = simplejson.loads(content)
140 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500141 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500142 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400143
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500144 fn = os.path.join(os.path.dirname(__file__), 'contrib',
145 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400146 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500147 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500148 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400149 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400150 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500151 future = None
152
153 return build_from_document(content, discoveryServiceUrl, future,
154 http, developerKey, model, requestBuilder)
155
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500156
Joe Gregorio292b9b82011-01-12 11:36:11 -0500157def build_from_document(
158 service,
159 base,
160 future=None,
161 http=None,
162 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500163 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500164 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500165 """Create a Resource for interacting with an API.
166
167 Same as `build()`, but constructs the Resource object
168 from a discovery document that is it given, as opposed to
169 retrieving one over HTTP.
170
Joe Gregorio292b9b82011-01-12 11:36:11 -0500171 Args:
172 service: string, discovery document
173 base: string, base URI for all HTTP requests, usually the discovery URI
174 future: string, discovery document with future capabilities
175 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500176 http: httplib2.Http, An instance of httplib2.Http or something that acts
177 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500178 developerKey: string, Key for controlling API usage, generated
179 from the API Console.
180 model: Model class instance that serializes and
181 de-serializes requests and responses.
182 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500183
184 Returns:
185 A Resource object with methods for interacting with
186 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500187 """
188
189 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400190 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500191 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500192 future = simplejson.loads(future)
193 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500194 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400195 future = {}
196 auth_discovery = {}
Joe Gregorio3c676f92011-07-25 10:38:14 -0400197 schema = service.get('schemas', {})
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400198
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500199 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500200 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500201 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500202 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400203 service, future, schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400204
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500205 def auth_method():
206 """Discovery information about the authentication the API uses."""
207 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400208
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500209 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400210
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500211 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400212
213
Joe Gregorio61d7e962011-02-22 22:52:07 -0500214def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500215 """Convert value to a string based on JSON Schema type.
216
217 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
218 JSON Schema.
219
220 Args:
221 value: any, the value to convert
222 schema_type: string, the type that value should be interpreted as
223
224 Returns:
225 A string representation of 'value' based on the schema_type.
226 """
227 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500228 if type(value) == type('') or type(value) == type(u''):
229 return value
230 else:
231 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500232 elif schema_type == 'integer':
233 return str(int(value))
234 elif schema_type == 'number':
235 return str(float(value))
236 elif schema_type == 'boolean':
237 return str(bool(value)).lower()
238 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500239 if type(value) == type('') or type(value) == type(u''):
240 return value
241 else:
242 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500243
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400244MULTIPLIERS = {
245 "KB": 2**10,
246 "MB": 2**20,
247 "GB": 2**30,
248 "TB": 2**40,
249 }
250
251def _media_size_to_long(maxSize):
252 """Convert a string media size, such as 10GB or 3TB into an integer."""
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400253 if len(maxSize) < 2:
254 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400255 units = maxSize[-2:].upper()
256 multiplier = MULTIPLIERS.get(units, 0)
257 if multiplier:
258 return int(maxSize[:-2])*multiplier
259 else:
260 return int(maxSize)
261
Joe Gregoriobee86832011-02-22 10:00:19 -0500262
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500263def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400264 developerKey, resourceDesc, futureDesc, schema):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400265
266 class Resource(object):
267 """A class for interacting with a resource."""
268
269 def __init__(self):
270 self._http = http
271 self._baseUrl = baseUrl
272 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400273 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500274 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400275
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400276 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400277 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400278 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400279 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400280 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400281
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400282 mediaPathUrl = None
283 accept = []
284 maxSize = 0
285 if 'mediaUpload' in methodDesc:
286 mediaUpload = methodDesc['mediaUpload']
287 mediaPathUrl = mediaUpload['protocols']['simple']['path']
288 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400289 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400290
Joe Gregorioca876e42011-02-22 19:39:42 -0500291 if 'parameters' not in methodDesc:
292 methodDesc['parameters'] = {}
293 for name in STACK_QUERY_PARAMETERS:
294 methodDesc['parameters'][name] = {
295 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400296 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500297 }
298
Joe Gregoriof4153422011-03-18 22:45:18 -0400299 if httpMethod in ['PUT', 'POST', 'PATCH']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500300 methodDesc['parameters']['body'] = {
301 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500302 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500303 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500304 }
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400305 if 'mediaUpload' in methodDesc:
306 methodDesc['parameters']['media_body'] = {
307 'description': 'The filename of the media request body.',
308 'type': 'string',
309 'required': False,
310 }
311 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100312
Joe Gregorioca876e42011-02-22 19:39:42 -0500313 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100314 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500315 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100316 pattern_params = {} # Parameters that must match a regex
317 query_params = [] # Parameters that will be used in the query string
318 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500319 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500320 enum_params = {} # Allowable enumeration values for each parameter
321
322
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400323 if 'parameters' in methodDesc:
324 for arg, desc in methodDesc['parameters'].iteritems():
325 param = key2param(arg)
326 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400327
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400328 if desc.get('pattern', ''):
329 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500330 if desc.get('enum', ''):
331 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400332 if desc.get('required', False):
333 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500334 if desc.get('repeated', False):
335 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400336 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400337 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400338 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400339 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500340 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400341
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500342 for match in URITEMPLATE.finditer(pathUrl):
343 for namematch in VARNAME.finditer(match.group(0)):
344 name = key2param(namematch.group(0))
345 path_params[name] = name
346 if name in query_params:
347 query_params.remove(name)
348
Joe Gregorio48d361f2010-08-18 13:19:21 -0400349 def method(self, **kwargs):
350 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500351 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400352 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400353
ade@google.com850cf552010-08-20 23:24:56 +0100354 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400355 if name not in kwargs:
356 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400357
ade@google.com850cf552010-08-20 23:24:56 +0100358 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400359 if name in kwargs:
360 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400361 raise TypeError(
362 'Parameter "%s" value "%s" does not match the pattern "%s"' %
363 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400364
Joe Gregoriobee86832011-02-22 10:00:19 -0500365 for name, enums in enum_params.iteritems():
366 if name in kwargs:
367 if kwargs[name] not in enums:
368 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500369 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500370 (name, kwargs[name], str(enums)))
371
ade@google.com850cf552010-08-20 23:24:56 +0100372 actual_query_params = {}
373 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400374 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500375 to_type = param_type.get(key, 'string')
376 # For repeated parameters we cast each member of the list.
377 if key in repeated_params and type(value) == type([]):
378 cast_value = [_cast(x, to_type) for x in value]
379 else:
380 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100381 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500382 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100383 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500384 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100385 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400386 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400387
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400388 if self._developerKey:
389 actual_query_params['key'] = self._developerKey
390
Joe Gregorio48d361f2010-08-18 13:19:21 -0400391 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400392 headers, params, query, body = self._model.request(headers,
393 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400394
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400395 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400396 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
397
398 if media_filename:
399 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
400 if media_mime_type is None:
401 raise UnknownFileType(media_filename)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400402 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
403 raise UnacceptableMimeTypeError(media_mime_type)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400404
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400405 # Check the maxSize
406 if maxSize > 0 and os.path.getsize(media_filename) > maxSize:
407 raise MediaUploadSizeError(media_filename)
408
409 # Use the media path uri for media uploads
410 expanded_url = uritemplate.expand(mediaPathUrl, params)
411 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400412
413 if body is None:
414 headers['content-type'] = media_mime_type
415 # make the body the contents of the file
416 f = file(media_filename, 'rb')
417 body = f.read()
418 f.close()
419 else:
420 msgRoot = MIMEMultipart('related')
421 # msgRoot should not write out it's own headers
422 setattr(msgRoot, '_write_headers', lambda self: None)
423
424 # attach the body as one part
425 msg = MIMENonMultipart(*headers['content-type'].split('/'))
426 msg.set_payload(body)
427 msgRoot.attach(msg)
428
429 # attach the media as the second part
430 msg = MIMENonMultipart(*media_mime_type.split('/'))
431 msg['Content-Transfer-Encoding'] = 'binary'
432
433 f = file(media_filename, 'rb')
434 msg.set_payload(f.read())
435 f.close()
436 msgRoot.attach(msg)
437
438 body = msgRoot.as_string()
439
440 # must appear after the call to as_string() to get the right boundary
441 headers['content-type'] = ('multipart/related; '
442 'boundary="%s"') % msgRoot.get_boundary()
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400443
ade@google.com850cf552010-08-20 23:24:56 +0100444 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500445 return self._requestBuilder(self._http,
446 self._model.response,
447 url,
448 method=httpMethod,
449 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500450 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500451 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400452
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500453 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
454 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500455 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400456 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500457 if arg in STACK_QUERY_PARAMETERS:
458 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500459 repeated = ''
460 if arg in repeated_params:
461 repeated = ' (repeated)'
462 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400463 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500464 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500465 paramdesc = methodDesc['parameters'][argmap[arg]]
466 paramdoc = paramdesc.get('description', 'A parameter')
467 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500468 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
469 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500470 enum = paramdesc.get('enum', [])
471 enumDesc = paramdesc.get('enumDescriptions', [])
472 if enum and enumDesc:
473 docs.append(' Allowed values\n')
474 for (name, desc) in zip(enum, enumDesc):
475 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400476
477 setattr(method, '__doc__', ''.join(docs))
478 setattr(theclass, methodName, method)
479
Joe Gregorio3c676f92011-07-25 10:38:14 -0400480 # This is a legacy method, as only Buzz and Moderator use the future.json
481 # functionality for generating _next methods. It will be kept around as long
482 # as those API versions are around, but no new APIs should depend upon it.
483 def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400484 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400485 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400486
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500487 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400488 """
489 Takes a single argument, 'body', which is the results
490 from the last call, and returns the next set of items
491 in the collection.
492
493 Returns None if there are no more items in
494 the collection.
495 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500496 if futureDesc['type'] != 'uri':
497 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400498
499 try:
500 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500501 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400502 p = p[key]
503 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400504 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400505 return None
506
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400507 if self._developerKey:
508 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100509 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400510 q.append(('key', self._developerKey))
511 parsed[4] = urllib.urlencode(q)
512 url = urlparse.urlunparse(parsed)
513
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400514 headers = {}
515 headers, params, query, body = self._model.request(headers, {}, {}, None)
516
517 logging.info('URL being requested: %s' % url)
518 resp, content = self._http.request(url, method='GET', headers=headers)
519
Joe Gregorioabda96f2011-02-11 20:19:33 -0500520 return self._requestBuilder(self._http,
521 self._model.response,
522 url,
523 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500524 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500525 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400526
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500527 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400528
Joe Gregorio3c676f92011-07-25 10:38:14 -0400529
530 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
531 methodName = _fix_method_name(methodName)
532 methodId = methodDesc['id'] + '.next'
533
534 def methodNext(self, previous_request, previous_response):
535 """Retrieves the next page of results.
536
537 Args:
538 previous_request: The request for the previous page.
539 previous_response: The response from the request for the previous page.
540
541 Returns:
542 A request object that you can call 'execute()' on to request the next
543 page. Returns None if there are no more items in the collection.
544 """
545 # Retrieve nextPageToken from previous_response
546 # Use as pageToken in previous_request to create new request.
547
548 if 'nextPageToken' not in previous_response:
549 return None
550
551 request = copy.copy(previous_request)
552
553 pageToken = previous_response['nextPageToken']
554 parsed = list(urlparse.urlparse(request.uri))
555 q = parse_qsl(parsed[4])
556
557 # Find and remove old 'pageToken' value from URI
558 newq = [(key, value) for (key, value) in q if key != 'pageToken']
559 newq.append(('pageToken', pageToken))
560 parsed[4] = urllib.urlencode(newq)
561 uri = urlparse.urlunparse(parsed)
562
563 request.uri = uri
564
565 logging.info('URL being requested: %s' % uri)
566
567 return request
568
569 setattr(theclass, methodName, methodNext)
570
571
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400572 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400573 if 'methods' in resourceDesc:
574 for methodName, methodDesc in resourceDesc['methods'].iteritems():
575 if futureDesc:
576 future = futureDesc['methods'].get(methodName, {})
577 else:
578 future = None
579 createMethod(Resource, methodName, methodDesc, future)
580
581 # Add in nested resources
582 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500583
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500584 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400585 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400586
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500587 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400588 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500589 self._requestBuilder, self._developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400590 methodDesc, futureDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400591
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500592 setattr(methodResource, '__doc__', 'A collection resource.')
593 setattr(methodResource, '__is_resource__', True)
594 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400595
596 for methodName, methodDesc in resourceDesc['resources'].iteritems():
597 if futureDesc and 'resources' in futureDesc:
598 future = futureDesc['resources'].get(methodName, {})
599 else:
600 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500601 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400602
603 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500604 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400605 for methodName, methodDesc in futureDesc['methods'].iteritems():
606 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio3c676f92011-07-25 10:38:14 -0400607 createNextMethodFromFuture(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500608 resourceDesc['methods'][methodName],
609 methodDesc['next'])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400610 # Add _next() methods
611 # Look for response bodies in schema that contain nextPageToken, and methods
612 # that take a pageToken parameter.
613 if 'methods' in resourceDesc:
614 for methodName, methodDesc in resourceDesc['methods'].iteritems():
615 if 'response' in methodDesc:
616 responseSchema = methodDesc['response']
617 if '$ref' in responseSchema:
618 responseSchema = schema[responseSchema['$ref']]
Joe Gregorio555f33c2011-08-19 14:56:07 -0400619 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
620 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400621 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
622 if hasNextPageToken and hasPageToken:
623 createNextMethod(Resource, methodName + '_next',
624 resourceDesc['methods'][methodName],
625 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400626
627 return Resource()