blob: 2a924a20946081fa4665741feb7c38bcc5e53560 [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 Gregorio562b7312011-09-15 09:06:38 -040062RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040063 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
64 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
65 'pass', 'print', 'raise', 'return', 'try', 'while' ]
66
Joe Gregorio562b7312011-09-15 09:06:38 -040067
Joe Gregoriod92897c2011-07-07 11:44:56 -040068def _fix_method_name(name):
69 if name in RESERVED_WORDS:
70 return name + '_'
71 else:
72 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040073
Joe Gregorio922b78c2011-05-26 21:36:34 -040074def _write_headers(self):
75 # Utility no-op method for multipart media handling
76 pass
77
78
Joe Gregorio48d361f2010-08-18 13:19:21 -040079def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050080 """Converts key names into parameter names.
81
82 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040083 """
84 result = []
85 key = list(key)
86 if not key[0].isalpha():
87 result.append('x')
88 for c in key:
89 if c.isalnum():
90 result.append(c)
91 else:
92 result.append('_')
93
94 return ''.join(result)
95
96
Joe Gregorioaf276d22010-12-09 14:26:58 -050097def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050098 http=None,
99 discoveryServiceUrl=DISCOVERY_URI,
100 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500101 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500102 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500103 """Construct a Resource for interacting with an API.
104
105 Construct a Resource object for interacting with
106 an API. The serviceName and version are the
107 names from the Discovery service.
108
109 Args:
110 serviceName: string, name of the service
111 version: string, the version of the service
112 discoveryServiceUrl: string, a URI Template that points to
113 the location of the discovery service. It should have two
114 parameters {api} and {apiVersion} that when filled in
115 produce an absolute URI to the discovery document for
116 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500117 developerKey: string, key obtained
118 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500119 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500120 requestBuilder: apiclient.http.HttpRequest, encapsulator for
121 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500122
123 Returns:
124 A Resource object with methods for interacting with
125 the service.
126 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400127 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400128 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400129 'apiVersion': version
130 }
ade@google.com850cf552010-08-20 23:24:56 +0100131
Joe Gregorioc204b642010-09-21 12:01:23 -0400132 if http is None:
133 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100134 requested_url = uritemplate.expand(discoveryServiceUrl, params)
135 logging.info('URL being requested: %s' % requested_url)
136 resp, content = http.request(requested_url)
Joe Gregorio49396552011-03-08 10:39:00 -0500137 if resp.status > 400:
138 raise HttpError(resp, content, requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500139 try:
140 service = simplejson.loads(content)
141 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500142 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500143 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400144
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500145 fn = os.path.join(os.path.dirname(__file__), 'contrib',
146 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400147 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500148 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500149 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400150 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400151 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500152 future = None
153
154 return build_from_document(content, discoveryServiceUrl, future,
155 http, developerKey, model, requestBuilder)
156
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500157
Joe Gregorio292b9b82011-01-12 11:36:11 -0500158def build_from_document(
159 service,
160 base,
161 future=None,
162 http=None,
163 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500164 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500165 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500166 """Create a Resource for interacting with an API.
167
168 Same as `build()`, but constructs the Resource object
169 from a discovery document that is it given, as opposed to
170 retrieving one over HTTP.
171
Joe Gregorio292b9b82011-01-12 11:36:11 -0500172 Args:
173 service: string, discovery document
174 base: string, base URI for all HTTP requests, usually the discovery URI
175 future: string, discovery document with future capabilities
176 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500177 http: httplib2.Http, An instance of httplib2.Http or something that acts
178 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500179 developerKey: string, Key for controlling API usage, generated
180 from the API Console.
181 model: Model class instance that serializes and
182 de-serializes requests and responses.
183 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500184
185 Returns:
186 A Resource object with methods for interacting with
187 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500188 """
189
190 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400191 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500192 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500193 future = simplejson.loads(future)
194 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500195 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400196 future = {}
197 auth_discovery = {}
Joe Gregorio3c676f92011-07-25 10:38:14 -0400198 schema = service.get('schemas', {})
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400199
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500200 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500201 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500202 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500203 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400204 service, future, schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400205
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500206 def auth_method():
207 """Discovery information about the authentication the API uses."""
208 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400209
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500210 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400211
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500212 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400213
214
Joe Gregorio61d7e962011-02-22 22:52:07 -0500215def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500216 """Convert value to a string based on JSON Schema type.
217
218 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
219 JSON Schema.
220
221 Args:
222 value: any, the value to convert
223 schema_type: string, the type that value should be interpreted as
224
225 Returns:
226 A string representation of 'value' based on the schema_type.
227 """
228 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500229 if type(value) == type('') or type(value) == type(u''):
230 return value
231 else:
232 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500233 elif schema_type == 'integer':
234 return str(int(value))
235 elif schema_type == 'number':
236 return str(float(value))
237 elif schema_type == 'boolean':
238 return str(bool(value)).lower()
239 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500240 if type(value) == type('') or type(value) == type(u''):
241 return value
242 else:
243 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500244
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400245MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400246 "KB": 2 ** 10,
247 "MB": 2 ** 20,
248 "GB": 2 ** 30,
249 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400250 }
251
252def _media_size_to_long(maxSize):
253 """Convert a string media size, such as 10GB or 3TB into an integer."""
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400254 if len(maxSize) < 2:
255 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400256 units = maxSize[-2:].upper()
257 multiplier = MULTIPLIERS.get(units, 0)
258 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400259 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400260 else:
261 return int(maxSize)
262
Joe Gregoriobee86832011-02-22 10:00:19 -0500263
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500264def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400265 developerKey, resourceDesc, futureDesc, schema):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400266
267 class Resource(object):
268 """A class for interacting with a resource."""
269
270 def __init__(self):
271 self._http = http
272 self._baseUrl = baseUrl
273 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400274 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500275 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400276
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400277 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400278 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400279 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400280 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400281 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400282
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400283 mediaPathUrl = None
284 accept = []
285 maxSize = 0
286 if 'mediaUpload' in methodDesc:
287 mediaUpload = methodDesc['mediaUpload']
288 mediaPathUrl = mediaUpload['protocols']['simple']['path']
289 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400290 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400291
Joe Gregorioca876e42011-02-22 19:39:42 -0500292 if 'parameters' not in methodDesc:
293 methodDesc['parameters'] = {}
294 for name in STACK_QUERY_PARAMETERS:
295 methodDesc['parameters'][name] = {
296 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400297 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500298 }
299
Joe Gregoriof4153422011-03-18 22:45:18 -0400300 if httpMethod in ['PUT', 'POST', 'PATCH']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500301 methodDesc['parameters']['body'] = {
302 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500303 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500304 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500305 }
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400306 if 'mediaUpload' in methodDesc:
307 methodDesc['parameters']['media_body'] = {
308 'description': 'The filename of the media request body.',
309 'type': 'string',
310 'required': False,
311 }
312 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100313
Joe Gregorioca876e42011-02-22 19:39:42 -0500314 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100315 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500316 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100317 pattern_params = {} # Parameters that must match a regex
318 query_params = [] # Parameters that will be used in the query string
319 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500320 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500321 enum_params = {} # Allowable enumeration values for each parameter
322
323
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400324 if 'parameters' in methodDesc:
325 for arg, desc in methodDesc['parameters'].iteritems():
326 param = key2param(arg)
327 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400328
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400329 if desc.get('pattern', ''):
330 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500331 if desc.get('enum', ''):
332 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400333 if desc.get('required', False):
334 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500335 if desc.get('repeated', False):
336 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400337 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400338 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400339 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400340 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500341 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400342
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500343 for match in URITEMPLATE.finditer(pathUrl):
344 for namematch in VARNAME.finditer(match.group(0)):
345 name = key2param(namematch.group(0))
346 path_params[name] = name
347 if name in query_params:
348 query_params.remove(name)
349
Joe Gregorio48d361f2010-08-18 13:19:21 -0400350 def method(self, **kwargs):
351 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500352 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400353 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400354
ade@google.com850cf552010-08-20 23:24:56 +0100355 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400356 if name not in kwargs:
357 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400358
ade@google.com850cf552010-08-20 23:24:56 +0100359 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400360 if name in kwargs:
361 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400362 raise TypeError(
363 'Parameter "%s" value "%s" does not match the pattern "%s"' %
364 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400365
Joe Gregoriobee86832011-02-22 10:00:19 -0500366 for name, enums in enum_params.iteritems():
367 if name in kwargs:
368 if kwargs[name] not in enums:
369 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500370 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500371 (name, kwargs[name], str(enums)))
372
ade@google.com850cf552010-08-20 23:24:56 +0100373 actual_query_params = {}
374 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400375 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500376 to_type = param_type.get(key, 'string')
377 # For repeated parameters we cast each member of the list.
378 if key in repeated_params and type(value) == type([]):
379 cast_value = [_cast(x, to_type) for x in value]
380 else:
381 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100382 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500383 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100384 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500385 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100386 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400387 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400388
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400389 if self._developerKey:
390 actual_query_params['key'] = self._developerKey
391
Joe Gregorio48d361f2010-08-18 13:19:21 -0400392 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400393 headers, params, query, body = self._model.request(headers,
394 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400395
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400396 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400397 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
398
399 if media_filename:
400 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
401 if media_mime_type is None:
402 raise UnknownFileType(media_filename)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400403 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
404 raise UnacceptableMimeTypeError(media_mime_type)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400405
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400406 # Check the maxSize
407 if maxSize > 0 and os.path.getsize(media_filename) > maxSize:
408 raise MediaUploadSizeError(media_filename)
409
410 # Use the media path uri for media uploads
411 expanded_url = uritemplate.expand(mediaPathUrl, params)
412 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400413
414 if body is None:
415 headers['content-type'] = media_mime_type
416 # make the body the contents of the file
417 f = file(media_filename, 'rb')
418 body = f.read()
419 f.close()
420 else:
421 msgRoot = MIMEMultipart('related')
422 # msgRoot should not write out it's own headers
423 setattr(msgRoot, '_write_headers', lambda self: None)
424
425 # attach the body as one part
426 msg = MIMENonMultipart(*headers['content-type'].split('/'))
427 msg.set_payload(body)
428 msgRoot.attach(msg)
429
430 # attach the media as the second part
431 msg = MIMENonMultipart(*media_mime_type.split('/'))
432 msg['Content-Transfer-Encoding'] = 'binary'
433
434 f = file(media_filename, 'rb')
435 msg.set_payload(f.read())
436 f.close()
437 msgRoot.attach(msg)
438
439 body = msgRoot.as_string()
440
441 # must appear after the call to as_string() to get the right boundary
442 headers['content-type'] = ('multipart/related; '
443 'boundary="%s"') % msgRoot.get_boundary()
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400444
ade@google.com850cf552010-08-20 23:24:56 +0100445 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500446 return self._requestBuilder(self._http,
447 self._model.response,
448 url,
449 method=httpMethod,
450 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500451 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500452 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400453
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500454 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
455 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500456 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400457 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500458 if arg in STACK_QUERY_PARAMETERS:
459 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500460 repeated = ''
461 if arg in repeated_params:
462 repeated = ' (repeated)'
463 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400464 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500465 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500466 paramdesc = methodDesc['parameters'][argmap[arg]]
467 paramdoc = paramdesc.get('description', 'A parameter')
468 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500469 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
470 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500471 enum = paramdesc.get('enum', [])
472 enumDesc = paramdesc.get('enumDescriptions', [])
473 if enum and enumDesc:
474 docs.append(' Allowed values\n')
475 for (name, desc) in zip(enum, enumDesc):
476 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400477
478 setattr(method, '__doc__', ''.join(docs))
479 setattr(theclass, methodName, method)
480
Joe Gregorio3c676f92011-07-25 10:38:14 -0400481 # This is a legacy method, as only Buzz and Moderator use the future.json
482 # functionality for generating _next methods. It will be kept around as long
483 # as those API versions are around, but no new APIs should depend upon it.
484 def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400485 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400486 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400487
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500488 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400489 """
490 Takes a single argument, 'body', which is the results
491 from the last call, and returns the next set of items
492 in the collection.
493
494 Returns None if there are no more items in
495 the collection.
496 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500497 if futureDesc['type'] != 'uri':
498 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400499
500 try:
501 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500502 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400503 p = p[key]
504 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400505 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400506 return None
507
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400508 if self._developerKey:
509 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100510 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400511 q.append(('key', self._developerKey))
512 parsed[4] = urllib.urlencode(q)
513 url = urlparse.urlunparse(parsed)
514
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400515 headers = {}
516 headers, params, query, body = self._model.request(headers, {}, {}, None)
517
518 logging.info('URL being requested: %s' % url)
519 resp, content = self._http.request(url, method='GET', headers=headers)
520
Joe Gregorioabda96f2011-02-11 20:19:33 -0500521 return self._requestBuilder(self._http,
522 self._model.response,
523 url,
524 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500525 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500526 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400527
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500528 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400529
Joe Gregorio3c676f92011-07-25 10:38:14 -0400530
531 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
532 methodName = _fix_method_name(methodName)
533 methodId = methodDesc['id'] + '.next'
534
535 def methodNext(self, previous_request, previous_response):
536 """Retrieves the next page of results.
537
538 Args:
539 previous_request: The request for the previous page.
540 previous_response: The response from the request for the previous page.
541
542 Returns:
543 A request object that you can call 'execute()' on to request the next
544 page. Returns None if there are no more items in the collection.
545 """
546 # Retrieve nextPageToken from previous_response
547 # Use as pageToken in previous_request to create new request.
548
549 if 'nextPageToken' not in previous_response:
550 return None
551
552 request = copy.copy(previous_request)
553
554 pageToken = previous_response['nextPageToken']
555 parsed = list(urlparse.urlparse(request.uri))
556 q = parse_qsl(parsed[4])
557
558 # Find and remove old 'pageToken' value from URI
559 newq = [(key, value) for (key, value) in q if key != 'pageToken']
560 newq.append(('pageToken', pageToken))
561 parsed[4] = urllib.urlencode(newq)
562 uri = urlparse.urlunparse(parsed)
563
564 request.uri = uri
565
566 logging.info('URL being requested: %s' % uri)
567
568 return request
569
570 setattr(theclass, methodName, methodNext)
571
572
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400573 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400574 if 'methods' in resourceDesc:
575 for methodName, methodDesc in resourceDesc['methods'].iteritems():
576 if futureDesc:
577 future = futureDesc['methods'].get(methodName, {})
578 else:
579 future = None
580 createMethod(Resource, methodName, methodDesc, future)
581
582 # Add in nested resources
583 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500584
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500585 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400586 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400587
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500588 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400589 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500590 self._requestBuilder, self._developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400591 methodDesc, futureDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400592
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500593 setattr(methodResource, '__doc__', 'A collection resource.')
594 setattr(methodResource, '__is_resource__', True)
595 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400596
597 for methodName, methodDesc in resourceDesc['resources'].iteritems():
598 if futureDesc and 'resources' in futureDesc:
599 future = futureDesc['resources'].get(methodName, {})
600 else:
601 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500602 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400603
604 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500605 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400606 for methodName, methodDesc in futureDesc['methods'].iteritems():
607 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio3c676f92011-07-25 10:38:14 -0400608 createNextMethodFromFuture(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500609 resourceDesc['methods'][methodName],
610 methodDesc['next'])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400611 # Add _next() methods
612 # Look for response bodies in schema that contain nextPageToken, and methods
613 # that take a pageToken parameter.
614 if 'methods' in resourceDesc:
615 for methodName, methodDesc in resourceDesc['methods'].iteritems():
616 if 'response' in methodDesc:
617 responseSchema = methodDesc['response']
618 if '$ref' in responseSchema:
619 responseSchema = schema[responseSchema['$ref']]
Joe Gregorio555f33c2011-08-19 14:56:07 -0400620 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
621 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400622 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
623 if hasNextPageToken and hasPageToken:
624 createNextMethod(Resource, methodName + '_next',
625 resourceDesc['methods'][methodName],
626 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400627
628 return Resource()