blob: 08ddc2646aba3a32a05e0f6eb2afce8eadbcf212 [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 Gregorio922b78c2011-05-26 21:36:34 -040032import mimetypes
33
ade@google.comc5eb46f2010-09-27 23:35:39 +010034try:
35 from urlparse import parse_qsl
36except ImportError:
37 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050038
Joe Gregorio034e7002010-12-15 08:45:03 -050039from anyjson import simplejson
Joe Gregorio922b78c2011-05-26 21:36:34 -040040from email.mime.multipart import MIMEMultipart
41from email.mime.nonmultipart import MIMENonMultipart
Joe Gregorioc0e0fe92011-03-04 16:16:55 -050042from errors import HttpError
Joe Gregorio49396552011-03-08 10:39:00 -050043from errors import InvalidJsonError
Joe Gregorio922b78c2011-05-26 21:36:34 -040044from errors import UnknownLinkType
45from http import HttpRequest
46from model import JsonModel
Joe Gregorio48d361f2010-08-18 13:19:21 -040047
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050048URITEMPLATE = re.compile('{[^}]*}')
49VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040050DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
51 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050052DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050053
54# Query parameters that work, but don't appear in discovery
Joe Gregorio06d852b2011-03-25 15:03:10 -040055STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
Joe Gregorio3eecaa92011-05-17 13:40:12 -040056 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040057
58
Joe Gregorio922b78c2011-05-26 21:36:34 -040059def _write_headers(self):
60 # Utility no-op method for multipart media handling
61 pass
62
63
Joe Gregorio48d361f2010-08-18 13:19:21 -040064def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050065 """Converts key names into parameter names.
66
67 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040068 """
69 result = []
70 key = list(key)
71 if not key[0].isalpha():
72 result.append('x')
73 for c in key:
74 if c.isalnum():
75 result.append(c)
76 else:
77 result.append('_')
78
79 return ''.join(result)
80
81
Joe Gregorioaf276d22010-12-09 14:26:58 -050082def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050083 http=None,
84 discoveryServiceUrl=DISCOVERY_URI,
85 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050086 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -050087 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050088 """Construct a Resource for interacting with an API.
89
90 Construct a Resource object for interacting with
91 an API. The serviceName and version are the
92 names from the Discovery service.
93
94 Args:
95 serviceName: string, name of the service
96 version: string, the version of the service
97 discoveryServiceUrl: string, a URI Template that points to
98 the location of the discovery service. It should have two
99 parameters {api} and {apiVersion} that when filled in
100 produce an absolute URI to the discovery document for
101 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500102 developerKey: string, key obtained
103 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500104 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500105 requestBuilder: apiclient.http.HttpRequest, encapsulator for
106 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500107
108 Returns:
109 A Resource object with methods for interacting with
110 the service.
111 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400112 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400113 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114 'apiVersion': version
115 }
ade@google.com850cf552010-08-20 23:24:56 +0100116
Joe Gregorioc204b642010-09-21 12:01:23 -0400117 if http is None:
118 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100119 requested_url = uritemplate.expand(discoveryServiceUrl, params)
120 logging.info('URL being requested: %s' % requested_url)
121 resp, content = http.request(requested_url)
Joe Gregorio49396552011-03-08 10:39:00 -0500122 if resp.status > 400:
123 raise HttpError(resp, content, requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500124 try:
125 service = simplejson.loads(content)
126 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500127 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500128 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400129
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500130 fn = os.path.join(os.path.dirname(__file__), 'contrib',
131 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400132 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500133 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500134 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400135 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400136 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500137 future = None
138
139 return build_from_document(content, discoveryServiceUrl, future,
140 http, developerKey, model, requestBuilder)
141
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500142
Joe Gregorio292b9b82011-01-12 11:36:11 -0500143def build_from_document(
144 service,
145 base,
146 future=None,
147 http=None,
148 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500149 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500150 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500151 """Create a Resource for interacting with an API.
152
153 Same as `build()`, but constructs the Resource object
154 from a discovery document that is it given, as opposed to
155 retrieving one over HTTP.
156
Joe Gregorio292b9b82011-01-12 11:36:11 -0500157 Args:
158 service: string, discovery document
159 base: string, base URI for all HTTP requests, usually the discovery URI
160 future: string, discovery document with future capabilities
161 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500162 http: httplib2.Http, An instance of httplib2.Http or something that acts
163 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500164 developerKey: string, Key for controlling API usage, generated
165 from the API Console.
166 model: Model class instance that serializes and
167 de-serializes requests and responses.
168 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500169
170 Returns:
171 A Resource object with methods for interacting with
172 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500173 """
174
175 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400176 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500177 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500178 future = simplejson.loads(future)
179 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500180 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400181 future = {}
182 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400183
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500184 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500185 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500186 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500187 resource = createResource(http, base, model, requestBuilder, developerKey,
188 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400189
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500190 def auth_method():
191 """Discovery information about the authentication the API uses."""
192 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400193
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500194 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400195
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500196 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400197
198
Joe Gregorio61d7e962011-02-22 22:52:07 -0500199def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500200 """Convert value to a string based on JSON Schema type.
201
202 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
203 JSON Schema.
204
205 Args:
206 value: any, the value to convert
207 schema_type: string, the type that value should be interpreted as
208
209 Returns:
210 A string representation of 'value' based on the schema_type.
211 """
212 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500213 if type(value) == type('') or type(value) == type(u''):
214 return value
215 else:
216 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500217 elif schema_type == 'integer':
218 return str(int(value))
219 elif schema_type == 'number':
220 return str(float(value))
221 elif schema_type == 'boolean':
222 return str(bool(value)).lower()
223 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500224 if type(value) == type('') or type(value) == type(u''):
225 return value
226 else:
227 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500228
229
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500230def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500231 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400232
233 class Resource(object):
234 """A class for interacting with a resource."""
235
236 def __init__(self):
237 self._http = http
238 self._baseUrl = baseUrl
239 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400240 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500241 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400242
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400243 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio6a63a762011-05-02 22:36:05 -0400244 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400245 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400246 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400247
Joe Gregorioca876e42011-02-22 19:39:42 -0500248 if 'parameters' not in methodDesc:
249 methodDesc['parameters'] = {}
250 for name in STACK_QUERY_PARAMETERS:
251 methodDesc['parameters'][name] = {
252 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400253 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500254 }
255
Joe Gregoriof4153422011-03-18 22:45:18 -0400256 if httpMethod in ['PUT', 'POST', 'PATCH']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500257 methodDesc['parameters']['body'] = {
258 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500259 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500260 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500261 }
Joe Gregorio922b78c2011-05-26 21:36:34 -0400262 methodDesc['parameters']['media_body'] = {
263 'description': 'The filename of the media request body.',
264 'type': 'string',
265 'required': False,
266 }
ade@google.com850cf552010-08-20 23:24:56 +0100267
Joe Gregorioca876e42011-02-22 19:39:42 -0500268 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100269 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500270 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100271 pattern_params = {} # Parameters that must match a regex
272 query_params = [] # Parameters that will be used in the query string
273 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500274 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500275 enum_params = {} # Allowable enumeration values for each parameter
276
277
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400278 if 'parameters' in methodDesc:
279 for arg, desc in methodDesc['parameters'].iteritems():
280 param = key2param(arg)
281 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400282
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400283 if desc.get('pattern', ''):
284 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500285 if desc.get('enum', ''):
286 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400287 if desc.get('required', False):
288 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500289 if desc.get('repeated', False):
290 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400291 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400292 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400293 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400294 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500295 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400296
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500297 for match in URITEMPLATE.finditer(pathUrl):
298 for namematch in VARNAME.finditer(match.group(0)):
299 name = key2param(namematch.group(0))
300 path_params[name] = name
301 if name in query_params:
302 query_params.remove(name)
303
Joe Gregorio48d361f2010-08-18 13:19:21 -0400304 def method(self, **kwargs):
305 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500306 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400307 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400308
ade@google.com850cf552010-08-20 23:24:56 +0100309 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400310 if name not in kwargs:
311 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400312
ade@google.com850cf552010-08-20 23:24:56 +0100313 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400314 if name in kwargs:
315 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400316 raise TypeError(
317 'Parameter "%s" value "%s" does not match the pattern "%s"' %
318 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400319
Joe Gregoriobee86832011-02-22 10:00:19 -0500320 for name, enums in enum_params.iteritems():
321 if name in kwargs:
322 if kwargs[name] not in enums:
323 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500324 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500325 (name, kwargs[name], str(enums)))
326
Joe Gregorio922b78c2011-05-26 21:36:34 -0400327 media_filename = kwargs.pop('media_body', None)
ade@google.com850cf552010-08-20 23:24:56 +0100328 actual_query_params = {}
329 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400330 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500331 to_type = param_type.get(key, 'string')
332 # For repeated parameters we cast each member of the list.
333 if key in repeated_params and type(value) == type([]):
334 cast_value = [_cast(x, to_type) for x in value]
335 else:
336 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100337 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500338 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100339 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500340 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100341 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400342
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400343 if self._developerKey:
344 actual_query_params['key'] = self._developerKey
345
Joe Gregorio48d361f2010-08-18 13:19:21 -0400346 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400347 headers, params, query, body = self._model.request(headers,
348 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400349
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400350 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400351 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
352
353 if media_filename:
354 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
355 if media_mime_type is None:
356 raise UnknownFileType(media_filename)
357
358 # modify the path to prepend '/upload'
359 parsed = list(urlparse.urlparse(url))
360 parsed[2] = '/upload' + parsed[2]
361 url = urlparse.urlunparse(parsed)
362
363 if body is None:
364 headers['content-type'] = media_mime_type
365 # make the body the contents of the file
366 f = file(media_filename, 'rb')
367 body = f.read()
368 f.close()
369 else:
370 msgRoot = MIMEMultipart('related')
371 # msgRoot should not write out it's own headers
372 setattr(msgRoot, '_write_headers', lambda self: None)
373
374 # attach the body as one part
375 msg = MIMENonMultipart(*headers['content-type'].split('/'))
376 msg.set_payload(body)
377 msgRoot.attach(msg)
378
379 # attach the media as the second part
380 msg = MIMENonMultipart(*media_mime_type.split('/'))
381 msg['Content-Transfer-Encoding'] = 'binary'
382
383 f = file(media_filename, 'rb')
384 msg.set_payload(f.read())
385 f.close()
386 msgRoot.attach(msg)
387
388 body = msgRoot.as_string()
389
390 # must appear after the call to as_string() to get the right boundary
391 headers['content-type'] = ('multipart/related; '
392 'boundary="%s"') % msgRoot.get_boundary()
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400393
ade@google.com850cf552010-08-20 23:24:56 +0100394 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500395 return self._requestBuilder(self._http,
396 self._model.response,
397 url,
398 method=httpMethod,
399 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500400 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500401 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400402
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500403 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
404 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500405 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400406 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500407 if arg in STACK_QUERY_PARAMETERS:
408 continue
Joe Gregorio922b78c2011-05-26 21:36:34 -0400409 if arg == 'media_body':
410 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500411 repeated = ''
412 if arg in repeated_params:
413 repeated = ' (repeated)'
414 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400415 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500416 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500417 paramdesc = methodDesc['parameters'][argmap[arg]]
418 paramdoc = paramdesc.get('description', 'A parameter')
419 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500420 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
421 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500422 enum = paramdesc.get('enum', [])
423 enumDesc = paramdesc.get('enumDescriptions', [])
424 if enum and enumDesc:
425 docs.append(' Allowed values\n')
426 for (name, desc) in zip(enum, enumDesc):
427 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400428
429 setattr(method, '__doc__', ''.join(docs))
430 setattr(theclass, methodName, method)
431
Joe Gregorioaf276d22010-12-09 14:26:58 -0500432 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio6a63a762011-05-02 22:36:05 -0400433 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400434
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500435 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400436 """
437 Takes a single argument, 'body', which is the results
438 from the last call, and returns the next set of items
439 in the collection.
440
441 Returns None if there are no more items in
442 the collection.
443 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500444 if futureDesc['type'] != 'uri':
445 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400446
447 try:
448 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500449 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400450 p = p[key]
451 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400452 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400453 return None
454
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400455 if self._developerKey:
456 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100457 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400458 q.append(('key', self._developerKey))
459 parsed[4] = urllib.urlencode(q)
460 url = urlparse.urlunparse(parsed)
461
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400462 headers = {}
463 headers, params, query, body = self._model.request(headers, {}, {}, None)
464
465 logging.info('URL being requested: %s' % url)
466 resp, content = self._http.request(url, method='GET', headers=headers)
467
Joe Gregorioabda96f2011-02-11 20:19:33 -0500468 return self._requestBuilder(self._http,
469 self._model.response,
470 url,
471 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500472 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500473 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400474
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500475 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400476
477 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400478 if 'methods' in resourceDesc:
479 for methodName, methodDesc in resourceDesc['methods'].iteritems():
480 if futureDesc:
481 future = futureDesc['methods'].get(methodName, {})
482 else:
483 future = None
484 createMethod(Resource, methodName, methodDesc, future)
485
486 # Add in nested resources
487 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500488
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500489 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400490
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500491 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400492 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500493 self._requestBuilder, self._developerKey,
494 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400495
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500496 setattr(methodResource, '__doc__', 'A collection resource.')
497 setattr(methodResource, '__is_resource__', True)
498 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400499
500 for methodName, methodDesc in resourceDesc['resources'].iteritems():
501 if futureDesc and 'resources' in futureDesc:
502 future = futureDesc['resources'].get(methodName, {})
503 else:
504 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500505 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400506
507 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500508 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400509 for methodName, methodDesc in futureDesc['methods'].iteritems():
510 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500511 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500512 resourceDesc['methods'][methodName],
513 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400514
515 return Resource()