blob: db4dd47d9c89431fe49c7b0344ae4f21cff1cdb8 [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
ade@google.comc5eb46f2010-09-27 23:35:39 +010032try:
33 from urlparse import parse_qsl
34except ImportError:
35 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050036
Joe Gregoriob843fa22010-12-13 16:26:07 -050037from http import HttpRequest
Joe Gregorio034e7002010-12-15 08:45:03 -050038from anyjson import simplejson
Joe Gregoriob843fa22010-12-13 16:26:07 -050039from model import JsonModel
Joe Gregoriob843fa22010-12-13 16:26:07 -050040from errors import UnknownLinkType
Joe Gregorio48d361f2010-08-18 13:19:21 -040041
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050042URITEMPLATE = re.compile('{[^}]*}')
43VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050044DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.3/describe/'
Joe Gregorio2379ecc2010-10-26 10:51:28 -040045 '{api}/{apiVersion}')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050046DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorio13217952011-02-22 15:37:38 -050047STACK_QUERY_PARAMETERS = ['trace']
Joe Gregorio48d361f2010-08-18 13:19:21 -040048
49
Joe Gregorio48d361f2010-08-18 13:19:21 -040050def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050051 """Converts key names into parameter names.
52
53 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040054 """
55 result = []
56 key = list(key)
57 if not key[0].isalpha():
58 result.append('x')
59 for c in key:
60 if c.isalnum():
61 result.append(c)
62 else:
63 result.append('_')
64
65 return ''.join(result)
66
67
Joe Gregorioaf276d22010-12-09 14:26:58 -050068def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050069 http=None,
70 discoveryServiceUrl=DISCOVERY_URI,
71 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050072 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -050073 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050074 """Construct a Resource for interacting with an API.
75
76 Construct a Resource object for interacting with
77 an API. The serviceName and version are the
78 names from the Discovery service.
79
80 Args:
81 serviceName: string, name of the service
82 version: string, the version of the service
83 discoveryServiceUrl: string, a URI Template that points to
84 the location of the discovery service. It should have two
85 parameters {api} and {apiVersion} that when filled in
86 produce an absolute URI to the discovery document for
87 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050088 developerKey: string, key obtained
89 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -050090 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -050091 requestBuilder: apiclient.http.HttpRequest, encapsulator for
92 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -050093
94 Returns:
95 A Resource object with methods for interacting with
96 the service.
97 """
Joe Gregorio48d361f2010-08-18 13:19:21 -040098 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040099 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400100 'apiVersion': version
101 }
ade@google.com850cf552010-08-20 23:24:56 +0100102
Joe Gregorioc204b642010-09-21 12:01:23 -0400103 if http is None:
104 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100105 requested_url = uritemplate.expand(discoveryServiceUrl, params)
106 logging.info('URL being requested: %s' % requested_url)
107 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400108 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400109
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500110 fn = os.path.join(os.path.dirname(__file__), 'contrib',
111 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400112 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500113 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500114 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400115 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400116 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500117 future = None
118
119 return build_from_document(content, discoveryServiceUrl, future,
120 http, developerKey, model, requestBuilder)
121
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500122
Joe Gregorio292b9b82011-01-12 11:36:11 -0500123def build_from_document(
124 service,
125 base,
126 future=None,
127 http=None,
128 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500129 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500130 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500131 """Create a Resource for interacting with an API.
132
133 Same as `build()`, but constructs the Resource object
134 from a discovery document that is it given, as opposed to
135 retrieving one over HTTP.
136
Joe Gregorio292b9b82011-01-12 11:36:11 -0500137 Args:
138 service: string, discovery document
139 base: string, base URI for all HTTP requests, usually the discovery URI
140 future: string, discovery document with future capabilities
141 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500142 http: httplib2.Http, An instance of httplib2.Http or something that acts
143 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500144 developerKey: string, Key for controlling API usage, generated
145 from the API Console.
146 model: Model class instance that serializes and
147 de-serializes requests and responses.
148 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500149
150 Returns:
151 A Resource object with methods for interacting with
152 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500153 """
154
155 service = simplejson.loads(service)
156 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500157 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500158 future = simplejson.loads(future)
159 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500160 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400161 future = {}
162 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400163
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500164 if model is None:
165 model = JsonModel('dataWrapper' in service.get('features', []))
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500166 resource = createResource(http, base, model, requestBuilder, developerKey,
167 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400168
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500169 def auth_method():
170 """Discovery information about the authentication the API uses."""
171 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500173 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400174
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500175 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400176
177
Joe Gregoriobee86832011-02-22 10:00:19 -0500178def _to_string(value, schema_type):
179 """Convert value to a string based on JSON Schema type.
180
181 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
182 JSON Schema.
183
184 Args:
185 value: any, the value to convert
186 schema_type: string, the type that value should be interpreted as
187
188 Returns:
189 A string representation of 'value' based on the schema_type.
190 """
191 if schema_type == 'string':
192 return str(value)
193 elif schema_type == 'integer':
194 return str(int(value))
195 elif schema_type == 'number':
196 return str(float(value))
197 elif schema_type == 'boolean':
198 return str(bool(value)).lower()
199 else:
200 return str(value)
201
202
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500203def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500204 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400205
206 class Resource(object):
207 """A class for interacting with a resource."""
208
209 def __init__(self):
210 self._http = http
211 self._baseUrl = baseUrl
212 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400213 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500214 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400215
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400216 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400217 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400218 pathUrl = re.sub(r'\{', r'{+', pathUrl)
219 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500220 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400221
ade@google.com850cf552010-08-20 23:24:56 +0100222 argmap = {}
223 if httpMethod in ['PUT', 'POST']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500224 if 'parameters' not in methodDesc:
225 methodDesc['parameters'] = {}
226 methodDesc['parameters']['body'] = {
227 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500228 'type': 'object',
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500229 }
ade@google.com850cf552010-08-20 23:24:56 +0100230
231 required_params = [] # Required parameters
232 pattern_params = {} # Parameters that must match a regex
233 query_params = [] # Parameters that will be used in the query string
234 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500235 param_type = {} # The type of the parameter
236 enum_params = {}
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400237 if 'parameters' in methodDesc:
238 for arg, desc in methodDesc['parameters'].iteritems():
239 param = key2param(arg)
240 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400241
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400242 if desc.get('pattern', ''):
243 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500244 if desc.get('enum', ''):
245 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400246 if desc.get('required', False):
247 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400248 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400249 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400250 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400251 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500252 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400253
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500254 for match in URITEMPLATE.finditer(pathUrl):
255 for namematch in VARNAME.finditer(match.group(0)):
256 name = key2param(namematch.group(0))
257 path_params[name] = name
258 if name in query_params:
259 query_params.remove(name)
260
Joe Gregorio48d361f2010-08-18 13:19:21 -0400261 def method(self, **kwargs):
262 for name in kwargs.iterkeys():
Joe Gregorio13217952011-02-22 15:37:38 -0500263 if name not in argmap and name not in STACK_QUERY_PARAMETERS:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400264 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400265
ade@google.com850cf552010-08-20 23:24:56 +0100266 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400267 if name not in kwargs:
268 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400269
ade@google.com850cf552010-08-20 23:24:56 +0100270 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400271 if name in kwargs:
272 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400273 raise TypeError(
274 'Parameter "%s" value "%s" does not match the pattern "%s"' %
275 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400276
Joe Gregoriobee86832011-02-22 10:00:19 -0500277 for name, enums in enum_params.iteritems():
278 if name in kwargs:
279 if kwargs[name] not in enums:
280 raise TypeError(
281 'Parameter "%s" value "%s" is not in the list of allowed values "%s"' %
282 (name, kwargs[name], str(enums)))
283
ade@google.com850cf552010-08-20 23:24:56 +0100284 actual_query_params = {}
285 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400286 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100287 if key in query_params:
Joe Gregoriobee86832011-02-22 10:00:19 -0500288 actual_query_params[argmap[key]] = _to_string(value, param_type[key])
ade@google.com850cf552010-08-20 23:24:56 +0100289 if key in path_params:
Joe Gregoriobee86832011-02-22 10:00:19 -0500290 actual_path_params[argmap[key]] = _to_string(value, param_type[key])
ade@google.com850cf552010-08-20 23:24:56 +0100291 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400292
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400293 if self._developerKey:
294 actual_query_params['key'] = self._developerKey
295
Joe Gregorio48d361f2010-08-18 13:19:21 -0400296 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400297 headers, params, query, body = self._model.request(headers,
298 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400299
Joe Gregorioaf276d22010-12-09 14:26:58 -0500300 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
301 # document. Base URLs should not contain any path elements. If they do
302 # then urlparse.urljoin will strip them out This results in an incorrect
303 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100304 url_result = urlparse.urlsplit(self._baseUrl)
305 new_base_url = url_result.scheme + '://' + url_result.netloc
306
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400307 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500308 url = urlparse.urljoin(new_base_url,
309 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400310
ade@google.com850cf552010-08-20 23:24:56 +0100311 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500312 return self._requestBuilder(self._http,
313 self._model.response,
314 url,
315 method=httpMethod,
316 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500317 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500318 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400319
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500320 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
321 if len(argmap) > 0:
322 docs.append("Args:\n")
Joe Gregorio48d361f2010-08-18 13:19:21 -0400323 for arg in argmap.iterkeys():
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500324 required = ""
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400325 if arg in required_params:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500326 required = " (required)"
Joe Gregorioc2a73932011-02-22 10:17:06 -0500327 paramdesc = methodDesc['parameters'][argmap[arg]]
328 paramdoc = paramdesc.get('description', 'A parameter')
329 paramtype = paramdesc.get('type', 'string')
330 docs.append(' %s: %s, %s%s\n' % (arg, paramtype, paramdoc, required))
331 enum = paramdesc.get('enum', [])
332 enumDesc = paramdesc.get('enumDescriptions', [])
333 if enum and enumDesc:
334 docs.append(' Allowed values\n')
335 for (name, desc) in zip(enum, enumDesc):
336 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400337
338 setattr(method, '__doc__', ''.join(docs))
339 setattr(theclass, methodName, method)
340
Joe Gregorioaf276d22010-12-09 14:26:58 -0500341 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
342 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400343
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500344 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400345 """
346 Takes a single argument, 'body', which is the results
347 from the last call, and returns the next set of items
348 in the collection.
349
350 Returns None if there are no more items in
351 the collection.
352 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500353 if futureDesc['type'] != 'uri':
354 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400355
356 try:
357 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500358 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400359 p = p[key]
360 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400361 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400362 return None
363
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400364 if self._developerKey:
365 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100366 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400367 q.append(('key', self._developerKey))
368 parsed[4] = urllib.urlencode(q)
369 url = urlparse.urlunparse(parsed)
370
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400371 headers = {}
372 headers, params, query, body = self._model.request(headers, {}, {}, None)
373
374 logging.info('URL being requested: %s' % url)
375 resp, content = self._http.request(url, method='GET', headers=headers)
376
Joe Gregorioabda96f2011-02-11 20:19:33 -0500377 return self._requestBuilder(self._http,
378 self._model.response,
379 url,
380 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500381 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500382 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400383
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500384 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400385
386 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400387 if 'methods' in resourceDesc:
388 for methodName, methodDesc in resourceDesc['methods'].iteritems():
389 if futureDesc:
390 future = futureDesc['methods'].get(methodName, {})
391 else:
392 future = None
393 createMethod(Resource, methodName, methodDesc, future)
394
395 # Add in nested resources
396 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500397
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500398 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400399
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500400 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400401 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500402 self._requestBuilder, self._developerKey,
403 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400404
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500405 setattr(methodResource, '__doc__', 'A collection resource.')
406 setattr(methodResource, '__is_resource__', True)
407 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400408
409 for methodName, methodDesc in resourceDesc['resources'].iteritems():
410 if futureDesc and 'resources' in futureDesc:
411 future = futureDesc['resources'].get(methodName, {})
412 else:
413 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500414 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400415
416 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500417 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400418 for methodName, methodDesc in futureDesc['methods'].iteritems():
419 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500420 createNextMethod(Resource, methodName + "_next",
Joe Gregorioaf276d22010-12-09 14:26:58 -0500421 resourceDesc['methods'][methodName],
422 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400423
424 return Resource()