blob: 7ae6a8a70d8020d6935919b1e749a0be5646171a [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 Gregorio48d361f2010-08-18 13:19:21 -040047
48
Joe Gregorio48d361f2010-08-18 13:19:21 -040049def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050050 """Converts key names into parameter names.
51
52 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040053 """
54 result = []
55 key = list(key)
56 if not key[0].isalpha():
57 result.append('x')
58 for c in key:
59 if c.isalnum():
60 result.append(c)
61 else:
62 result.append('_')
63
64 return ''.join(result)
65
66
Joe Gregorioaf276d22010-12-09 14:26:58 -050067def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050068 http=None,
69 discoveryServiceUrl=DISCOVERY_URI,
70 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050071 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -050072 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050073 """Construct a Resource for interacting with an API.
74
75 Construct a Resource object for interacting with
76 an API. The serviceName and version are the
77 names from the Discovery service.
78
79 Args:
80 serviceName: string, name of the service
81 version: string, the version of the service
82 discoveryServiceUrl: string, a URI Template that points to
83 the location of the discovery service. It should have two
84 parameters {api} and {apiVersion} that when filled in
85 produce an absolute URI to the discovery document for
86 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050087 developerKey: string, key obtained
88 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -050089 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -050090 requestBuilder: apiclient.http.HttpRequest, encapsulator for
91 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -050092
93 Returns:
94 A Resource object with methods for interacting with
95 the service.
96 """
Joe Gregorio48d361f2010-08-18 13:19:21 -040097 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040098 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -040099 'apiVersion': version
100 }
ade@google.com850cf552010-08-20 23:24:56 +0100101
Joe Gregorioc204b642010-09-21 12:01:23 -0400102 if http is None:
103 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100104 requested_url = uritemplate.expand(discoveryServiceUrl, params)
105 logging.info('URL being requested: %s' % requested_url)
106 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400107 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400108
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500109 fn = os.path.join(os.path.dirname(__file__), 'contrib',
110 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400111 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500112 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500113 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400114 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400115 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500116 future = None
117
118 return build_from_document(content, discoveryServiceUrl, future,
119 http, developerKey, model, requestBuilder)
120
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500121
Joe Gregorio292b9b82011-01-12 11:36:11 -0500122def build_from_document(
123 service,
124 base,
125 future=None,
126 http=None,
127 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500128 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500129 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500130 """Create a Resource for interacting with an API.
131
132 Same as `build()`, but constructs the Resource object
133 from a discovery document that is it given, as opposed to
134 retrieving one over HTTP.
135
Joe Gregorio292b9b82011-01-12 11:36:11 -0500136 Args:
137 service: string, discovery document
138 base: string, base URI for all HTTP requests, usually the discovery URI
139 future: string, discovery document with future capabilities
140 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500141 http: httplib2.Http, An instance of httplib2.Http or something that acts
142 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500143 developerKey: string, Key for controlling API usage, generated
144 from the API Console.
145 model: Model class instance that serializes and
146 de-serializes requests and responses.
147 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500148
149 Returns:
150 A Resource object with methods for interacting with
151 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500152 """
153
154 service = simplejson.loads(service)
155 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500156 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500157 future = simplejson.loads(future)
158 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500159 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400160 future = {}
161 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400162
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500163 if model is None:
164 model = JsonModel('dataWrapper' in service.get('features', []))
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500165 resource = createResource(http, base, model, requestBuilder, developerKey,
166 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400167
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500168 def auth_method():
169 """Discovery information about the authentication the API uses."""
170 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400171
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500172 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400173
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500174 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175
176
Joe Gregoriobee86832011-02-22 10:00:19 -0500177def _to_string(value, schema_type):
178 """Convert value to a string based on JSON Schema type.
179
180 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
181 JSON Schema.
182
183 Args:
184 value: any, the value to convert
185 schema_type: string, the type that value should be interpreted as
186
187 Returns:
188 A string representation of 'value' based on the schema_type.
189 """
190 if schema_type == 'string':
191 return str(value)
192 elif schema_type == 'integer':
193 return str(int(value))
194 elif schema_type == 'number':
195 return str(float(value))
196 elif schema_type == 'boolean':
197 return str(bool(value)).lower()
198 else:
199 return str(value)
200
201
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500202def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500203 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400204
205 class Resource(object):
206 """A class for interacting with a resource."""
207
208 def __init__(self):
209 self._http = http
210 self._baseUrl = baseUrl
211 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400212 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500213 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400214
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400215 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400216 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400217 pathUrl = re.sub(r'\{', r'{+', pathUrl)
218 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500219 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400220
ade@google.com850cf552010-08-20 23:24:56 +0100221 argmap = {}
222 if httpMethod in ['PUT', 'POST']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500223 if 'parameters' not in methodDesc:
224 methodDesc['parameters'] = {}
225 methodDesc['parameters']['body'] = {
226 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500227 'type': 'object',
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500228 }
ade@google.com850cf552010-08-20 23:24:56 +0100229
230 required_params = [] # Required parameters
231 pattern_params = {} # Parameters that must match a regex
232 query_params = [] # Parameters that will be used in the query string
233 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500234 param_type = {} # The type of the parameter
235 enum_params = {}
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400236 if 'parameters' in methodDesc:
237 for arg, desc in methodDesc['parameters'].iteritems():
238 param = key2param(arg)
239 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400240
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400241 if desc.get('pattern', ''):
242 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500243 if desc.get('enum', ''):
244 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400245 if desc.get('required', False):
246 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400247 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400248 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400249 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400250 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500251 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400252
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500253 for match in URITEMPLATE.finditer(pathUrl):
254 for namematch in VARNAME.finditer(match.group(0)):
255 name = key2param(namematch.group(0))
256 path_params[name] = name
257 if name in query_params:
258 query_params.remove(name)
259
Joe Gregorio48d361f2010-08-18 13:19:21 -0400260 def method(self, **kwargs):
261 for name in kwargs.iterkeys():
262 if name not in argmap:
263 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400264
ade@google.com850cf552010-08-20 23:24:56 +0100265 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400266 if name not in kwargs:
267 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400268
ade@google.com850cf552010-08-20 23:24:56 +0100269 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400270 if name in kwargs:
271 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400272 raise TypeError(
273 'Parameter "%s" value "%s" does not match the pattern "%s"' %
274 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400275
Joe Gregoriobee86832011-02-22 10:00:19 -0500276 for name, enums in enum_params.iteritems():
277 if name in kwargs:
278 if kwargs[name] not in enums:
279 raise TypeError(
280 'Parameter "%s" value "%s" is not in the list of allowed values "%s"' %
281 (name, kwargs[name], str(enums)))
282
ade@google.com850cf552010-08-20 23:24:56 +0100283 actual_query_params = {}
284 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400285 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100286 if key in query_params:
Joe Gregoriobee86832011-02-22 10:00:19 -0500287 actual_query_params[argmap[key]] = _to_string(value, param_type[key])
ade@google.com850cf552010-08-20 23:24:56 +0100288 if key in path_params:
Joe Gregoriobee86832011-02-22 10:00:19 -0500289 actual_path_params[argmap[key]] = _to_string(value, param_type[key])
ade@google.com850cf552010-08-20 23:24:56 +0100290 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400291
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400292 if self._developerKey:
293 actual_query_params['key'] = self._developerKey
294
Joe Gregorio48d361f2010-08-18 13:19:21 -0400295 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400296 headers, params, query, body = self._model.request(headers,
297 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400298
Joe Gregorioaf276d22010-12-09 14:26:58 -0500299 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
300 # document. Base URLs should not contain any path elements. If they do
301 # then urlparse.urljoin will strip them out This results in an incorrect
302 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100303 url_result = urlparse.urlsplit(self._baseUrl)
304 new_base_url = url_result.scheme + '://' + url_result.netloc
305
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400306 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500307 url = urlparse.urljoin(new_base_url,
308 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400309
ade@google.com850cf552010-08-20 23:24:56 +0100310 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500311 return self._requestBuilder(self._http,
312 self._model.response,
313 url,
314 method=httpMethod,
315 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500316 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500317 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400318
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500319 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
320 if len(argmap) > 0:
321 docs.append("Args:\n")
Joe Gregorio48d361f2010-08-18 13:19:21 -0400322 for arg in argmap.iterkeys():
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500323 required = ""
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400324 if arg in required_params:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500325 required = " (required)"
Joe Gregorioc2a73932011-02-22 10:17:06 -0500326 paramdesc = methodDesc['parameters'][argmap[arg]]
327 paramdoc = paramdesc.get('description', 'A parameter')
328 paramtype = paramdesc.get('type', 'string')
329 docs.append(' %s: %s, %s%s\n' % (arg, paramtype, paramdoc, required))
330 enum = paramdesc.get('enum', [])
331 enumDesc = paramdesc.get('enumDescriptions', [])
332 if enum and enumDesc:
333 docs.append(' Allowed values\n')
334 for (name, desc) in zip(enum, enumDesc):
335 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400336
337 setattr(method, '__doc__', ''.join(docs))
338 setattr(theclass, methodName, method)
339
Joe Gregorioaf276d22010-12-09 14:26:58 -0500340 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
341 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400342
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500343 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400344 """
345 Takes a single argument, 'body', which is the results
346 from the last call, and returns the next set of items
347 in the collection.
348
349 Returns None if there are no more items in
350 the collection.
351 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500352 if futureDesc['type'] != 'uri':
353 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400354
355 try:
356 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500357 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400358 p = p[key]
359 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400360 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400361 return None
362
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400363 if self._developerKey:
364 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100365 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400366 q.append(('key', self._developerKey))
367 parsed[4] = urllib.urlencode(q)
368 url = urlparse.urlunparse(parsed)
369
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400370 headers = {}
371 headers, params, query, body = self._model.request(headers, {}, {}, None)
372
373 logging.info('URL being requested: %s' % url)
374 resp, content = self._http.request(url, method='GET', headers=headers)
375
Joe Gregorioabda96f2011-02-11 20:19:33 -0500376 return self._requestBuilder(self._http,
377 self._model.response,
378 url,
379 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500380 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500381 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400382
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500383 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400384
385 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400386 if 'methods' in resourceDesc:
387 for methodName, methodDesc in resourceDesc['methods'].iteritems():
388 if futureDesc:
389 future = futureDesc['methods'].get(methodName, {})
390 else:
391 future = None
392 createMethod(Resource, methodName, methodDesc, future)
393
394 # Add in nested resources
395 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500396
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500397 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400398
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500399 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400400 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500401 self._requestBuilder, self._developerKey,
402 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400403
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500404 setattr(methodResource, '__doc__', 'A collection resource.')
405 setattr(methodResource, '__is_resource__', True)
406 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400407
408 for methodName, methodDesc in resourceDesc['resources'].iteritems():
409 if futureDesc and 'resources' in futureDesc:
410 future = futureDesc['resources'].get(methodName, {})
411 else:
412 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500413 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400414
415 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500416 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400417 for methodName, methodDesc in futureDesc['methods'].iteritems():
418 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500419 createNextMethod(Resource, methodName + "_next",
Joe Gregorioaf276d22010-12-09 14:26:58 -0500420 resourceDesc['methods'][methodName],
421 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400422
423 return Resource()