blob: 6f0f7520e59d9222ef5e9ffd1624e11219e81002 [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
61
Joe Gregorio922b78c2011-05-26 21:36:34 -040062def _write_headers(self):
63 # Utility no-op method for multipart media handling
64 pass
65
66
Joe Gregorio48d361f2010-08-18 13:19:21 -040067def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050068 """Converts key names into parameter names.
69
70 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040071 """
72 result = []
73 key = list(key)
74 if not key[0].isalpha():
75 result.append('x')
76 for c in key:
77 if c.isalnum():
78 result.append(c)
79 else:
80 result.append('_')
81
82 return ''.join(result)
83
84
Joe Gregorioaf276d22010-12-09 14:26:58 -050085def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050086 http=None,
87 discoveryServiceUrl=DISCOVERY_URI,
88 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050089 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -050090 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050091 """Construct a Resource for interacting with an API.
92
93 Construct a Resource object for interacting with
94 an API. The serviceName and version are the
95 names from the Discovery service.
96
97 Args:
98 serviceName: string, name of the service
99 version: string, the version of the service
100 discoveryServiceUrl: string, a URI Template that points to
101 the location of the discovery service. It should have two
102 parameters {api} and {apiVersion} that when filled in
103 produce an absolute URI to the discovery document for
104 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500105 developerKey: string, key obtained
106 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500107 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500108 requestBuilder: apiclient.http.HttpRequest, encapsulator for
109 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500110
111 Returns:
112 A Resource object with methods for interacting with
113 the service.
114 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400115 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400116 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400117 'apiVersion': version
118 }
ade@google.com850cf552010-08-20 23:24:56 +0100119
Joe Gregorioc204b642010-09-21 12:01:23 -0400120 if http is None:
121 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100122 requested_url = uritemplate.expand(discoveryServiceUrl, params)
123 logging.info('URL being requested: %s' % requested_url)
124 resp, content = http.request(requested_url)
Joe Gregorio49396552011-03-08 10:39:00 -0500125 if resp.status > 400:
126 raise HttpError(resp, content, requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500127 try:
128 service = simplejson.loads(content)
129 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500130 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500131 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400132
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500133 fn = os.path.join(os.path.dirname(__file__), 'contrib',
134 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400135 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500136 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500137 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400138 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400139 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500140 future = None
141
142 return build_from_document(content, discoveryServiceUrl, future,
143 http, developerKey, model, requestBuilder)
144
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500145
Joe Gregorio292b9b82011-01-12 11:36:11 -0500146def build_from_document(
147 service,
148 base,
149 future=None,
150 http=None,
151 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500152 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500153 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500154 """Create a Resource for interacting with an API.
155
156 Same as `build()`, but constructs the Resource object
157 from a discovery document that is it given, as opposed to
158 retrieving one over HTTP.
159
Joe Gregorio292b9b82011-01-12 11:36:11 -0500160 Args:
161 service: string, discovery document
162 base: string, base URI for all HTTP requests, usually the discovery URI
163 future: string, discovery document with future capabilities
164 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500165 http: httplib2.Http, An instance of httplib2.Http or something that acts
166 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500167 developerKey: string, Key for controlling API usage, generated
168 from the API Console.
169 model: Model class instance that serializes and
170 de-serializes requests and responses.
171 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500172
173 Returns:
174 A Resource object with methods for interacting with
175 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500176 """
177
178 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400179 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500180 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500181 future = simplejson.loads(future)
182 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500183 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400184 future = {}
185 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400186
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500187 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500188 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500189 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500190 resource = createResource(http, base, model, requestBuilder, developerKey,
191 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400192
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500193 def auth_method():
194 """Discovery information about the authentication the API uses."""
195 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400196
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500197 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400198
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500199 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400200
201
Joe Gregorio61d7e962011-02-22 22:52:07 -0500202def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500203 """Convert value to a string based on JSON Schema type.
204
205 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
206 JSON Schema.
207
208 Args:
209 value: any, the value to convert
210 schema_type: string, the type that value should be interpreted as
211
212 Returns:
213 A string representation of 'value' based on the schema_type.
214 """
215 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500216 if type(value) == type('') or type(value) == type(u''):
217 return value
218 else:
219 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500220 elif schema_type == 'integer':
221 return str(int(value))
222 elif schema_type == 'number':
223 return str(float(value))
224 elif schema_type == 'boolean':
225 return str(bool(value)).lower()
226 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500227 if type(value) == type('') or type(value) == type(u''):
228 return value
229 else:
230 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500231
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400232MULTIPLIERS = {
233 "KB": 2**10,
234 "MB": 2**20,
235 "GB": 2**30,
236 "TB": 2**40,
237 }
238
239def _media_size_to_long(maxSize):
240 """Convert a string media size, such as 10GB or 3TB into an integer."""
241 units = maxSize[-2:].upper()
242 multiplier = MULTIPLIERS.get(units, 0)
243 if multiplier:
244 return int(maxSize[:-2])*multiplier
245 else:
246 return int(maxSize)
247
Joe Gregoriobee86832011-02-22 10:00:19 -0500248
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500249def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500250 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400251
252 class Resource(object):
253 """A class for interacting with a resource."""
254
255 def __init__(self):
256 self._http = http
257 self._baseUrl = baseUrl
258 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400259 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500260 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400261
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400262 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio6a63a762011-05-02 22:36:05 -0400263 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400264 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400265 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400266
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400267 mediaPathUrl = None
268 accept = []
269 maxSize = 0
270 if 'mediaUpload' in methodDesc:
271 mediaUpload = methodDesc['mediaUpload']
272 mediaPathUrl = mediaUpload['protocols']['simple']['path']
273 accept = mediaUpload['accept']
274 maxSize = _media_size_to_long(mediaUpload['maxSize'])
275
Joe Gregorioca876e42011-02-22 19:39:42 -0500276 if 'parameters' not in methodDesc:
277 methodDesc['parameters'] = {}
278 for name in STACK_QUERY_PARAMETERS:
279 methodDesc['parameters'][name] = {
280 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400281 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500282 }
283
Joe Gregoriof4153422011-03-18 22:45:18 -0400284 if httpMethod in ['PUT', 'POST', 'PATCH']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500285 methodDesc['parameters']['body'] = {
286 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500287 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500288 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500289 }
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400290 if 'mediaUpload' in methodDesc:
291 methodDesc['parameters']['media_body'] = {
292 'description': 'The filename of the media request body.',
293 'type': 'string',
294 'required': False,
295 }
296 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100297
Joe Gregorioca876e42011-02-22 19:39:42 -0500298 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100299 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500300 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100301 pattern_params = {} # Parameters that must match a regex
302 query_params = [] # Parameters that will be used in the query string
303 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500304 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500305 enum_params = {} # Allowable enumeration values for each parameter
306
307
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400308 if 'parameters' in methodDesc:
309 for arg, desc in methodDesc['parameters'].iteritems():
310 param = key2param(arg)
311 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400312
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400313 if desc.get('pattern', ''):
314 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500315 if desc.get('enum', ''):
316 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400317 if desc.get('required', False):
318 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500319 if desc.get('repeated', False):
320 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400321 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400322 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400323 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400324 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500325 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400326
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500327 for match in URITEMPLATE.finditer(pathUrl):
328 for namematch in VARNAME.finditer(match.group(0)):
329 name = key2param(namematch.group(0))
330 path_params[name] = name
331 if name in query_params:
332 query_params.remove(name)
333
Joe Gregorio48d361f2010-08-18 13:19:21 -0400334 def method(self, **kwargs):
335 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500336 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400337 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400338
ade@google.com850cf552010-08-20 23:24:56 +0100339 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400340 if name not in kwargs:
341 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400342
ade@google.com850cf552010-08-20 23:24:56 +0100343 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400344 if name in kwargs:
345 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400346 raise TypeError(
347 'Parameter "%s" value "%s" does not match the pattern "%s"' %
348 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400349
Joe Gregoriobee86832011-02-22 10:00:19 -0500350 for name, enums in enum_params.iteritems():
351 if name in kwargs:
352 if kwargs[name] not in enums:
353 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500354 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500355 (name, kwargs[name], str(enums)))
356
ade@google.com850cf552010-08-20 23:24:56 +0100357 actual_query_params = {}
358 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400359 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500360 to_type = param_type.get(key, 'string')
361 # For repeated parameters we cast each member of the list.
362 if key in repeated_params and type(value) == type([]):
363 cast_value = [_cast(x, to_type) for x in value]
364 else:
365 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100366 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500367 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100368 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500369 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100370 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400371 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400372
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400373 if self._developerKey:
374 actual_query_params['key'] = self._developerKey
375
Joe Gregorio48d361f2010-08-18 13:19:21 -0400376 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400377 headers, params, query, body = self._model.request(headers,
378 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400379
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400380 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400381 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
382
383 if media_filename:
384 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
385 if media_mime_type is None:
386 raise UnknownFileType(media_filename)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400387 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
388 raise UnacceptableMimeTypeError(media_mime_type)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400389
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400390 # Check the maxSize
391 if maxSize > 0 and os.path.getsize(media_filename) > maxSize:
392 raise MediaUploadSizeError(media_filename)
393
394 # Use the media path uri for media uploads
395 expanded_url = uritemplate.expand(mediaPathUrl, params)
396 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400397
398 if body is None:
399 headers['content-type'] = media_mime_type
400 # make the body the contents of the file
401 f = file(media_filename, 'rb')
402 body = f.read()
403 f.close()
404 else:
405 msgRoot = MIMEMultipart('related')
406 # msgRoot should not write out it's own headers
407 setattr(msgRoot, '_write_headers', lambda self: None)
408
409 # attach the body as one part
410 msg = MIMENonMultipart(*headers['content-type'].split('/'))
411 msg.set_payload(body)
412 msgRoot.attach(msg)
413
414 # attach the media as the second part
415 msg = MIMENonMultipart(*media_mime_type.split('/'))
416 msg['Content-Transfer-Encoding'] = 'binary'
417
418 f = file(media_filename, 'rb')
419 msg.set_payload(f.read())
420 f.close()
421 msgRoot.attach(msg)
422
423 body = msgRoot.as_string()
424
425 # must appear after the call to as_string() to get the right boundary
426 headers['content-type'] = ('multipart/related; '
427 'boundary="%s"') % msgRoot.get_boundary()
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400428
ade@google.com850cf552010-08-20 23:24:56 +0100429 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500430 return self._requestBuilder(self._http,
431 self._model.response,
432 url,
433 method=httpMethod,
434 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500435 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500436 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400437
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500438 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
439 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500440 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400441 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500442 if arg in STACK_QUERY_PARAMETERS:
443 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500444 repeated = ''
445 if arg in repeated_params:
446 repeated = ' (repeated)'
447 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400448 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500449 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500450 paramdesc = methodDesc['parameters'][argmap[arg]]
451 paramdoc = paramdesc.get('description', 'A parameter')
452 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500453 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
454 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500455 enum = paramdesc.get('enum', [])
456 enumDesc = paramdesc.get('enumDescriptions', [])
457 if enum and enumDesc:
458 docs.append(' Allowed values\n')
459 for (name, desc) in zip(enum, enumDesc):
460 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400461
462 setattr(method, '__doc__', ''.join(docs))
463 setattr(theclass, methodName, method)
464
Joe Gregorioaf276d22010-12-09 14:26:58 -0500465 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio6a63a762011-05-02 22:36:05 -0400466 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400467
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500468 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400469 """
470 Takes a single argument, 'body', which is the results
471 from the last call, and returns the next set of items
472 in the collection.
473
474 Returns None if there are no more items in
475 the collection.
476 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500477 if futureDesc['type'] != 'uri':
478 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400479
480 try:
481 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500482 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400483 p = p[key]
484 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400485 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400486 return None
487
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400488 if self._developerKey:
489 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100490 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400491 q.append(('key', self._developerKey))
492 parsed[4] = urllib.urlencode(q)
493 url = urlparse.urlunparse(parsed)
494
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400495 headers = {}
496 headers, params, query, body = self._model.request(headers, {}, {}, None)
497
498 logging.info('URL being requested: %s' % url)
499 resp, content = self._http.request(url, method='GET', headers=headers)
500
Joe Gregorioabda96f2011-02-11 20:19:33 -0500501 return self._requestBuilder(self._http,
502 self._model.response,
503 url,
504 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500505 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500506 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400507
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500508 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400509
510 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400511 if 'methods' in resourceDesc:
512 for methodName, methodDesc in resourceDesc['methods'].iteritems():
513 if futureDesc:
514 future = futureDesc['methods'].get(methodName, {})
515 else:
516 future = None
517 createMethod(Resource, methodName, methodDesc, future)
518
519 # Add in nested resources
520 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500521
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500522 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400523
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500524 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400525 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500526 self._requestBuilder, self._developerKey,
527 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400528
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500529 setattr(methodResource, '__doc__', 'A collection resource.')
530 setattr(methodResource, '__is_resource__', True)
531 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400532
533 for methodName, methodDesc in resourceDesc['resources'].iteritems():
534 if futureDesc and 'resources' in futureDesc:
535 future = futureDesc['resources'].get(methodName, {})
536 else:
537 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500538 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400539
540 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500541 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400542 for methodName, methodDesc in futureDesc['methods'].iteritems():
543 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500544 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500545 resourceDesc['methods'][methodName],
546 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400547
548 return Resource()