blob: df01aa0468896d65b93851641caa573f5bc728c5 [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 Gregorioc0e0fe92011-03-04 16:16:55 -050041from errors import HttpError
Joe Gregorio49396552011-03-08 10:39:00 -050042from errors import InvalidJsonError
Joe Gregorio48d361f2010-08-18 13:19:21 -040043
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050044URITEMPLATE = re.compile('{[^}]*}')
45VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050046DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.3/describe/'
Joe Gregorio2379ecc2010-10-26 10:51:28 -040047 '{api}/{apiVersion}')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050048DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050049
50# Query parameters that work, but don't appear in discovery
Joe Gregorioe9e236f2011-03-21 22:23:14 -040051STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040052
53
Joe Gregorio48d361f2010-08-18 13:19:21 -040054def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050055 """Converts key names into parameter names.
56
57 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040058 """
59 result = []
60 key = list(key)
61 if not key[0].isalpha():
62 result.append('x')
63 for c in key:
64 if c.isalnum():
65 result.append(c)
66 else:
67 result.append('_')
68
69 return ''.join(result)
70
71
Joe Gregorioaf276d22010-12-09 14:26:58 -050072def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050073 http=None,
74 discoveryServiceUrl=DISCOVERY_URI,
75 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050076 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -050077 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050078 """Construct a Resource for interacting with an API.
79
80 Construct a Resource object for interacting with
81 an API. The serviceName and version are the
82 names from the Discovery service.
83
84 Args:
85 serviceName: string, name of the service
86 version: string, the version of the service
87 discoveryServiceUrl: string, a URI Template that points to
88 the location of the discovery service. It should have two
89 parameters {api} and {apiVersion} that when filled in
90 produce an absolute URI to the discovery document for
91 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050092 developerKey: string, key obtained
93 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -050094 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -050095 requestBuilder: apiclient.http.HttpRequest, encapsulator for
96 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -050097
98 Returns:
99 A Resource object with methods for interacting with
100 the service.
101 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400102 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400103 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400104 'apiVersion': version
105 }
ade@google.com850cf552010-08-20 23:24:56 +0100106
Joe Gregorioc204b642010-09-21 12:01:23 -0400107 if http is None:
108 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100109 requested_url = uritemplate.expand(discoveryServiceUrl, params)
110 logging.info('URL being requested: %s' % requested_url)
111 resp, content = http.request(requested_url)
Joe Gregorio49396552011-03-08 10:39:00 -0500112 if resp.status > 400:
113 raise HttpError(resp, content, requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500114 try:
115 service = simplejson.loads(content)
116 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500117 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500118 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400119
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500120 fn = os.path.join(os.path.dirname(__file__), 'contrib',
121 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400122 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500123 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500124 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400125 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400126 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500127 future = None
128
129 return build_from_document(content, discoveryServiceUrl, future,
130 http, developerKey, model, requestBuilder)
131
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500132
Joe Gregorio292b9b82011-01-12 11:36:11 -0500133def build_from_document(
134 service,
135 base,
136 future=None,
137 http=None,
138 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500139 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500140 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500141 """Create a Resource for interacting with an API.
142
143 Same as `build()`, but constructs the Resource object
144 from a discovery document that is it given, as opposed to
145 retrieving one over HTTP.
146
Joe Gregorio292b9b82011-01-12 11:36:11 -0500147 Args:
148 service: string, discovery document
149 base: string, base URI for all HTTP requests, usually the discovery URI
150 future: string, discovery document with future capabilities
151 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500152 http: httplib2.Http, An instance of httplib2.Http or something that acts
153 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500154 developerKey: string, Key for controlling API usage, generated
155 from the API Console.
156 model: Model class instance that serializes and
157 de-serializes requests and responses.
158 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500159
160 Returns:
161 A Resource object with methods for interacting with
162 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500163 """
164
165 service = simplejson.loads(service)
166 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500167 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500168 future = simplejson.loads(future)
169 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500170 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400171 future = {}
172 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400173
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500174 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500175 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500176 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500177 resource = createResource(http, base, model, requestBuilder, developerKey,
178 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400179
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500180 def auth_method():
181 """Discovery information about the authentication the API uses."""
182 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400183
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500184 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400185
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500186 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400187
188
Joe Gregorio61d7e962011-02-22 22:52:07 -0500189def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500190 """Convert value to a string based on JSON Schema type.
191
192 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
193 JSON Schema.
194
195 Args:
196 value: any, the value to convert
197 schema_type: string, the type that value should be interpreted as
198
199 Returns:
200 A string representation of 'value' based on the schema_type.
201 """
202 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500203 if type(value) == type('') or type(value) == type(u''):
204 return value
205 else:
206 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500207 elif schema_type == 'integer':
208 return str(int(value))
209 elif schema_type == 'number':
210 return str(float(value))
211 elif schema_type == 'boolean':
212 return str(bool(value)).lower()
213 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500214 if type(value) == type('') or type(value) == type(u''):
215 return value
216 else:
217 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500218
219
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500220def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500221 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400222
223 class Resource(object):
224 """A class for interacting with a resource."""
225
226 def __init__(self):
227 self._http = http
228 self._baseUrl = baseUrl
229 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400230 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500231 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400232
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400233 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400234 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400235 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500236 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400237
Joe Gregorioca876e42011-02-22 19:39:42 -0500238 if 'parameters' not in methodDesc:
239 methodDesc['parameters'] = {}
240 for name in STACK_QUERY_PARAMETERS:
241 methodDesc['parameters'][name] = {
242 'type': 'string',
243 'restParameterType': 'query'
244 }
245
Joe Gregoriof4153422011-03-18 22:45:18 -0400246 if httpMethod in ['PUT', 'POST', 'PATCH']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500247 methodDesc['parameters']['body'] = {
248 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500249 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500250 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500251 }
ade@google.com850cf552010-08-20 23:24:56 +0100252
Joe Gregorioca876e42011-02-22 19:39:42 -0500253 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100254 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500255 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100256 pattern_params = {} # Parameters that must match a regex
257 query_params = [] # Parameters that will be used in the query string
258 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500259 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500260 enum_params = {} # Allowable enumeration values for each parameter
261
262
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400263 if 'parameters' in methodDesc:
264 for arg, desc in methodDesc['parameters'].iteritems():
265 param = key2param(arg)
266 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400267
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400268 if desc.get('pattern', ''):
269 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500270 if desc.get('enum', ''):
271 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400272 if desc.get('required', False):
273 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500274 if desc.get('repeated', False):
275 repeated_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400276 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400277 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400278 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400279 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500280 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400281
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500282 for match in URITEMPLATE.finditer(pathUrl):
283 for namematch in VARNAME.finditer(match.group(0)):
284 name = key2param(namematch.group(0))
285 path_params[name] = name
286 if name in query_params:
287 query_params.remove(name)
288
Joe Gregorio48d361f2010-08-18 13:19:21 -0400289 def method(self, **kwargs):
290 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500291 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400292 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400293
ade@google.com850cf552010-08-20 23:24:56 +0100294 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400295 if name not in kwargs:
296 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400297
ade@google.com850cf552010-08-20 23:24:56 +0100298 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400299 if name in kwargs:
300 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400301 raise TypeError(
302 'Parameter "%s" value "%s" does not match the pattern "%s"' %
303 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400304
Joe Gregoriobee86832011-02-22 10:00:19 -0500305 for name, enums in enum_params.iteritems():
306 if name in kwargs:
307 if kwargs[name] not in enums:
308 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500309 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500310 (name, kwargs[name], str(enums)))
311
ade@google.com850cf552010-08-20 23:24:56 +0100312 actual_query_params = {}
313 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400314 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500315 to_type = param_type.get(key, 'string')
316 # For repeated parameters we cast each member of the list.
317 if key in repeated_params and type(value) == type([]):
318 cast_value = [_cast(x, to_type) for x in value]
319 else:
320 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100321 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500322 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100323 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500324 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100325 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400326
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400327 if self._developerKey:
328 actual_query_params['key'] = self._developerKey
329
Joe Gregorio48d361f2010-08-18 13:19:21 -0400330 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400331 headers, params, query, body = self._model.request(headers,
332 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400333
Joe Gregorioaf276d22010-12-09 14:26:58 -0500334 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
335 # document. Base URLs should not contain any path elements. If they do
336 # then urlparse.urljoin will strip them out This results in an incorrect
337 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100338 url_result = urlparse.urlsplit(self._baseUrl)
Joe Gregorio6f3014a2011-03-18 21:52:42 -0400339 new_base_url = url_result[0] + '://' + url_result[1]
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100340
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400341 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio6f3014a2011-03-18 21:52:42 -0400342 url = urlparse.urljoin(self._baseUrl,
343 url_result[2] + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400344
ade@google.com850cf552010-08-20 23:24:56 +0100345 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500346 return self._requestBuilder(self._http,
347 self._model.response,
348 url,
349 method=httpMethod,
350 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500351 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500352 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400353
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500354 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
355 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500356 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400357 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500358 if arg in STACK_QUERY_PARAMETERS:
359 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500360 repeated = ''
361 if arg in repeated_params:
362 repeated = ' (repeated)'
363 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400364 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500365 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500366 paramdesc = methodDesc['parameters'][argmap[arg]]
367 paramdoc = paramdesc.get('description', 'A parameter')
368 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500369 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
370 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500371 enum = paramdesc.get('enum', [])
372 enumDesc = paramdesc.get('enumDescriptions', [])
373 if enum and enumDesc:
374 docs.append(' Allowed values\n')
375 for (name, desc) in zip(enum, enumDesc):
376 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400377
378 setattr(method, '__doc__', ''.join(docs))
379 setattr(theclass, methodName, method)
380
Joe Gregorioaf276d22010-12-09 14:26:58 -0500381 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
382 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400383
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500384 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400385 """
386 Takes a single argument, 'body', which is the results
387 from the last call, and returns the next set of items
388 in the collection.
389
390 Returns None if there are no more items in
391 the collection.
392 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500393 if futureDesc['type'] != 'uri':
394 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400395
396 try:
397 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500398 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400399 p = p[key]
400 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400401 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400402 return None
403
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400404 if self._developerKey:
405 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100406 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400407 q.append(('key', self._developerKey))
408 parsed[4] = urllib.urlencode(q)
409 url = urlparse.urlunparse(parsed)
410
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400411 headers = {}
412 headers, params, query, body = self._model.request(headers, {}, {}, None)
413
414 logging.info('URL being requested: %s' % url)
415 resp, content = self._http.request(url, method='GET', headers=headers)
416
Joe Gregorioabda96f2011-02-11 20:19:33 -0500417 return self._requestBuilder(self._http,
418 self._model.response,
419 url,
420 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500421 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500422 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400423
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500424 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400425
426 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400427 if 'methods' in resourceDesc:
428 for methodName, methodDesc in resourceDesc['methods'].iteritems():
429 if futureDesc:
430 future = futureDesc['methods'].get(methodName, {})
431 else:
432 future = None
433 createMethod(Resource, methodName, methodDesc, future)
434
435 # Add in nested resources
436 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500437
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500438 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400439
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500440 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400441 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500442 self._requestBuilder, self._developerKey,
443 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400444
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500445 setattr(methodResource, '__doc__', 'A collection resource.')
446 setattr(methodResource, '__is_resource__', True)
447 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400448
449 for methodName, methodDesc in resourceDesc['resources'].iteritems():
450 if futureDesc and 'resources' in futureDesc:
451 future = futureDesc['resources'].get(methodName, {})
452 else:
453 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500454 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400455
456 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500457 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400458 for methodName, methodDesc in futureDesc['methods'].iteritems():
459 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500460 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500461 resourceDesc['methods'][methodName],
462 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400463
464 return Resource()