blob: 2965f3c1153b3e42132d9d8fc0fbec025ec6e897 [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
25import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010026import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040027import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040028import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040029import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040030import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040031import urlparse
Joe Gregoriofdf7c802011-06-30 12:33:38 -040032import mimeparse
Joe Gregorio922b78c2011-05-26 21:36:34 -040033import mimetypes
34
ade@google.comc5eb46f2010-09-27 23:35:39 +010035try:
36 from urlparse import parse_qsl
37except ImportError:
38 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050039
Joe Gregorio034e7002010-12-15 08:45:03 -050040from anyjson import simplejson
Joe Gregorio922b78c2011-05-26 21:36:34 -040041from email.mime.multipart import MIMEMultipart
42from email.mime.nonmultipart import MIMENonMultipart
Joe Gregorioc0e0fe92011-03-04 16:16:55 -050043from errors import HttpError
Joe Gregorio49396552011-03-08 10:39:00 -050044from errors import InvalidJsonError
Joe Gregoriofdf7c802011-06-30 12:33:38 -040045from errors import MediaUploadSizeError
46from errors import UnacceptableMimeTypeError
Joe Gregorio922b78c2011-05-26 21:36:34 -040047from errors import UnknownLinkType
48from http import HttpRequest
49from model import JsonModel
Joe Gregorio48d361f2010-08-18 13:19:21 -040050
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050051URITEMPLATE = re.compile('{[^}]*}')
52VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040053DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
54 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050055DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050056
57# Query parameters that work, but don't appear in discovery
Joe Gregorio06d852b2011-03-25 15:03:10 -040058STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
Joe Gregorio3eecaa92011-05-17 13:40:12 -040059 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040060
Joe Gregoriod92897c2011-07-07 11:44:56 -040061RESERVED_WORDS = [ 'and', 'assert', 'break', 'class', 'continue', 'def', 'del',
62 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
63 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
64 'pass', 'print', 'raise', 'return', 'try', 'while' ]
65
66def _fix_method_name(name):
67 if name in RESERVED_WORDS:
68 return name + '_'
69 else:
70 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040071
Joe Gregorio922b78c2011-05-26 21:36:34 -040072def _write_headers(self):
73 # Utility no-op method for multipart media handling
74 pass
75
76
Joe Gregorio48d361f2010-08-18 13:19:21 -040077def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050078 """Converts key names into parameter names.
79
80 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040081 """
82 result = []
83 key = list(key)
84 if not key[0].isalpha():
85 result.append('x')
86 for c in key:
87 if c.isalnum():
88 result.append(c)
89 else:
90 result.append('_')
91
92 return ''.join(result)
93
94
Joe Gregorioaf276d22010-12-09 14:26:58 -050095def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050096 http=None,
97 discoveryServiceUrl=DISCOVERY_URI,
98 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050099 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500100 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500101 """Construct a Resource for interacting with an API.
102
103 Construct a Resource object for interacting with
104 an API. The serviceName and version are the
105 names from the Discovery service.
106
107 Args:
108 serviceName: string, name of the service
109 version: string, the version of the service
110 discoveryServiceUrl: string, a URI Template that points to
111 the location of the discovery service. It should have two
112 parameters {api} and {apiVersion} that when filled in
113 produce an absolute URI to the discovery document for
114 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500115 developerKey: string, key obtained
116 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500117 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500118 requestBuilder: apiclient.http.HttpRequest, encapsulator for
119 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500120
121 Returns:
122 A Resource object with methods for interacting with
123 the service.
124 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400125 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400126 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400127 'apiVersion': version
128 }
ade@google.com850cf552010-08-20 23:24:56 +0100129
Joe Gregorioc204b642010-09-21 12:01:23 -0400130 if http is None:
131 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100132 requested_url = uritemplate.expand(discoveryServiceUrl, params)
133 logging.info('URL being requested: %s' % requested_url)
134 resp, content = http.request(requested_url)
Joe Gregorio49396552011-03-08 10:39:00 -0500135 if resp.status > 400:
136 raise HttpError(resp, content, requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500137 try:
138 service = simplejson.loads(content)
139 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500140 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500141 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400142
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500143 fn = os.path.join(os.path.dirname(__file__), 'contrib',
144 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400145 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500146 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500147 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400148 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400149 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500150 future = None
151
152 return build_from_document(content, discoveryServiceUrl, future,
153 http, developerKey, model, requestBuilder)
154
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500155
Joe Gregorio292b9b82011-01-12 11:36:11 -0500156def build_from_document(
157 service,
158 base,
159 future=None,
160 http=None,
161 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500162 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500163 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500164 """Create a Resource for interacting with an API.
165
166 Same as `build()`, but constructs the Resource object
167 from a discovery document that is it given, as opposed to
168 retrieving one over HTTP.
169
Joe Gregorio292b9b82011-01-12 11:36:11 -0500170 Args:
171 service: string, discovery document
172 base: string, base URI for all HTTP requests, usually the discovery URI
173 future: string, discovery document with future capabilities
174 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500175 http: httplib2.Http, An instance of httplib2.Http or something that acts
176 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500177 developerKey: string, Key for controlling API usage, generated
178 from the API Console.
179 model: Model class instance that serializes and
180 de-serializes requests and responses.
181 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500182
183 Returns:
184 A Resource object with methods for interacting with
185 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500186 """
187
188 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400189 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500190 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500191 future = simplejson.loads(future)
192 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500193 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400194 future = {}
195 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400196
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500197 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500198 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500199 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500200 resource = createResource(http, base, model, requestBuilder, developerKey,
201 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400202
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500203 def auth_method():
204 """Discovery information about the authentication the API uses."""
205 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400206
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500207 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400208
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500209 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400210
211
Joe Gregorio61d7e962011-02-22 22:52:07 -0500212def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500213 """Convert value to a string based on JSON Schema type.
214
215 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
216 JSON Schema.
217
218 Args:
219 value: any, the value to convert
220 schema_type: string, the type that value should be interpreted as
221
222 Returns:
223 A string representation of 'value' based on the schema_type.
224 """
225 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500226 if type(value) == type('') or type(value) == type(u''):
227 return value
228 else:
229 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500230 elif schema_type == 'integer':
231 return str(int(value))
232 elif schema_type == 'number':
233 return str(float(value))
234 elif schema_type == 'boolean':
235 return str(bool(value)).lower()
236 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500237 if type(value) == type('') or type(value) == type(u''):
238 return value
239 else:
240 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500241
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400242MULTIPLIERS = {
243 "KB": 2**10,
244 "MB": 2**20,
245 "GB": 2**30,
246 "TB": 2**40,
247 }
248
249def _media_size_to_long(maxSize):
250 """Convert a string media size, such as 10GB or 3TB into an integer."""
251 units = maxSize[-2:].upper()
252 multiplier = MULTIPLIERS.get(units, 0)
253 if multiplier:
254 return int(maxSize[:-2])*multiplier
255 else:
256 return int(maxSize)
257
Joe Gregoriobee86832011-02-22 10:00:19 -0500258
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500259def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500260 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400261
262 class Resource(object):
263 """A class for interacting with a resource."""
264
265 def __init__(self):
266 self._http = http
267 self._baseUrl = baseUrl
268 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400269 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500270 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400271
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400272 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400273 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400274 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400275 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400276 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400277
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400278 mediaPathUrl = None
279 accept = []
280 maxSize = 0
281 if 'mediaUpload' in methodDesc:
282 mediaUpload = methodDesc['mediaUpload']
283 mediaPathUrl = mediaUpload['protocols']['simple']['path']
284 accept = mediaUpload['accept']
285 maxSize = _media_size_to_long(mediaUpload['maxSize'])
286
Joe Gregorioca876e42011-02-22 19:39:42 -0500287 if 'parameters' not in methodDesc:
288 methodDesc['parameters'] = {}
289 for name in STACK_QUERY_PARAMETERS:
290 methodDesc['parameters'][name] = {
291 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400292 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500293 }
294
Joe Gregoriof4153422011-03-18 22:45:18 -0400295 if httpMethod in ['PUT', 'POST', 'PATCH']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500296 methodDesc['parameters']['body'] = {
297 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500298 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500299 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500300 }
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400301 if 'mediaUpload' in methodDesc:
302 methodDesc['parameters']['media_body'] = {
303 'description': 'The filename of the media request body.',
304 'type': 'string',
305 'required': False,
306 }
307 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100308
Joe Gregorioca876e42011-02-22 19:39:42 -0500309 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100310 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500311 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100312 pattern_params = {} # Parameters that must match a regex
313 query_params = [] # Parameters that will be used in the query string
314 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500315 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500316 enum_params = {} # Allowable enumeration values for each parameter
317
318
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400319 if 'parameters' in methodDesc:
320 for arg, desc in methodDesc['parameters'].iteritems():
321 param = key2param(arg)
322 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400323
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400324 if desc.get('pattern', ''):
325 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500326 if desc.get('enum', ''):
327 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400328 if desc.get('required', False):
329 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500330 if desc.get('repeated', False):
331 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400332 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400333 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400334 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400335 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500336 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400337
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500338 for match in URITEMPLATE.finditer(pathUrl):
339 for namematch in VARNAME.finditer(match.group(0)):
340 name = key2param(namematch.group(0))
341 path_params[name] = name
342 if name in query_params:
343 query_params.remove(name)
344
Joe Gregorio48d361f2010-08-18 13:19:21 -0400345 def method(self, **kwargs):
346 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500347 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400348 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400349
ade@google.com850cf552010-08-20 23:24:56 +0100350 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400351 if name not in kwargs:
352 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400353
ade@google.com850cf552010-08-20 23:24:56 +0100354 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400355 if name in kwargs:
356 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400357 raise TypeError(
358 'Parameter "%s" value "%s" does not match the pattern "%s"' %
359 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400360
Joe Gregoriobee86832011-02-22 10:00:19 -0500361 for name, enums in enum_params.iteritems():
362 if name in kwargs:
363 if kwargs[name] not in enums:
364 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500365 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500366 (name, kwargs[name], str(enums)))
367
ade@google.com850cf552010-08-20 23:24:56 +0100368 actual_query_params = {}
369 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400370 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500371 to_type = param_type.get(key, 'string')
372 # For repeated parameters we cast each member of the list.
373 if key in repeated_params and type(value) == type([]):
374 cast_value = [_cast(x, to_type) for x in value]
375 else:
376 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100377 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500378 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100379 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500380 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100381 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400382 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400383
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400384 if self._developerKey:
385 actual_query_params['key'] = self._developerKey
386
Joe Gregorio48d361f2010-08-18 13:19:21 -0400387 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400388 headers, params, query, body = self._model.request(headers,
389 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400390
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400391 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400392 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
393
394 if media_filename:
395 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
396 if media_mime_type is None:
397 raise UnknownFileType(media_filename)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400398 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
399 raise UnacceptableMimeTypeError(media_mime_type)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400400
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400401 # Check the maxSize
402 if maxSize > 0 and os.path.getsize(media_filename) > maxSize:
403 raise MediaUploadSizeError(media_filename)
404
405 # Use the media path uri for media uploads
406 expanded_url = uritemplate.expand(mediaPathUrl, params)
407 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400408
409 if body is None:
410 headers['content-type'] = media_mime_type
411 # make the body the contents of the file
412 f = file(media_filename, 'rb')
413 body = f.read()
414 f.close()
415 else:
416 msgRoot = MIMEMultipart('related')
417 # msgRoot should not write out it's own headers
418 setattr(msgRoot, '_write_headers', lambda self: None)
419
420 # attach the body as one part
421 msg = MIMENonMultipart(*headers['content-type'].split('/'))
422 msg.set_payload(body)
423 msgRoot.attach(msg)
424
425 # attach the media as the second part
426 msg = MIMENonMultipart(*media_mime_type.split('/'))
427 msg['Content-Transfer-Encoding'] = 'binary'
428
429 f = file(media_filename, 'rb')
430 msg.set_payload(f.read())
431 f.close()
432 msgRoot.attach(msg)
433
434 body = msgRoot.as_string()
435
436 # must appear after the call to as_string() to get the right boundary
437 headers['content-type'] = ('multipart/related; '
438 'boundary="%s"') % msgRoot.get_boundary()
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400439
ade@google.com850cf552010-08-20 23:24:56 +0100440 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500441 return self._requestBuilder(self._http,
442 self._model.response,
443 url,
444 method=httpMethod,
445 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500446 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500447 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400448
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500449 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
450 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500451 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400452 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500453 if arg in STACK_QUERY_PARAMETERS:
454 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500455 repeated = ''
456 if arg in repeated_params:
457 repeated = ' (repeated)'
458 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400459 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500460 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500461 paramdesc = methodDesc['parameters'][argmap[arg]]
462 paramdoc = paramdesc.get('description', 'A parameter')
463 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500464 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
465 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500466 enum = paramdesc.get('enum', [])
467 enumDesc = paramdesc.get('enumDescriptions', [])
468 if enum and enumDesc:
469 docs.append(' Allowed values\n')
470 for (name, desc) in zip(enum, enumDesc):
471 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400472
473 setattr(method, '__doc__', ''.join(docs))
474 setattr(theclass, methodName, method)
475
Joe Gregorioaf276d22010-12-09 14:26:58 -0500476 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400477 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400478 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400479
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500480 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400481 """
482 Takes a single argument, 'body', which is the results
483 from the last call, and returns the next set of items
484 in the collection.
485
486 Returns None if there are no more items in
487 the collection.
488 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500489 if futureDesc['type'] != 'uri':
490 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400491
492 try:
493 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500494 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400495 p = p[key]
496 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400497 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400498 return None
499
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400500 if self._developerKey:
501 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100502 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400503 q.append(('key', self._developerKey))
504 parsed[4] = urllib.urlencode(q)
505 url = urlparse.urlunparse(parsed)
506
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400507 headers = {}
508 headers, params, query, body = self._model.request(headers, {}, {}, None)
509
510 logging.info('URL being requested: %s' % url)
511 resp, content = self._http.request(url, method='GET', headers=headers)
512
Joe Gregorioabda96f2011-02-11 20:19:33 -0500513 return self._requestBuilder(self._http,
514 self._model.response,
515 url,
516 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500517 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500518 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400519
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500520 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400521
522 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400523 if 'methods' in resourceDesc:
524 for methodName, methodDesc in resourceDesc['methods'].iteritems():
525 if futureDesc:
526 future = futureDesc['methods'].get(methodName, {})
527 else:
528 future = None
529 createMethod(Resource, methodName, methodDesc, future)
530
531 # Add in nested resources
532 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500533
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500534 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400535 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400536
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500537 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400538 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500539 self._requestBuilder, self._developerKey,
540 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400541
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500542 setattr(methodResource, '__doc__', 'A collection resource.')
543 setattr(methodResource, '__is_resource__', True)
544 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400545
546 for methodName, methodDesc in resourceDesc['resources'].iteritems():
547 if futureDesc and 'resources' in futureDesc:
548 future = futureDesc['resources'].get(methodName, {})
549 else:
550 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500551 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400552
553 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500554 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400555 for methodName, methodDesc in futureDesc['methods'].iteritems():
556 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500557 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500558 resourceDesc['methods'][methodName],
559 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400560
561 return Resource()