blob: cc31967c88e14be45223cb10dec107c0943e33f5 [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 Gregorio13217952011-02-22 15:37:38 -050051STACK_QUERY_PARAMETERS = ['trace']
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 Gregorio49396552011-03-08 10:39:00 -0500117 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400118
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500119 fn = os.path.join(os.path.dirname(__file__), 'contrib',
120 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400121 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500122 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500123 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400124 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400125 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500126 future = None
127
128 return build_from_document(content, discoveryServiceUrl, future,
129 http, developerKey, model, requestBuilder)
130
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500131
Joe Gregorio292b9b82011-01-12 11:36:11 -0500132def build_from_document(
133 service,
134 base,
135 future=None,
136 http=None,
137 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500138 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500139 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500140 """Create a Resource for interacting with an API.
141
142 Same as `build()`, but constructs the Resource object
143 from a discovery document that is it given, as opposed to
144 retrieving one over HTTP.
145
Joe Gregorio292b9b82011-01-12 11:36:11 -0500146 Args:
147 service: string, discovery document
148 base: string, base URI for all HTTP requests, usually the discovery URI
149 future: string, discovery document with future capabilities
150 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500151 http: httplib2.Http, An instance of httplib2.Http or something that acts
152 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500153 developerKey: string, Key for controlling API usage, generated
154 from the API Console.
155 model: Model class instance that serializes and
156 de-serializes requests and responses.
157 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500158
159 Returns:
160 A Resource object with methods for interacting with
161 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500162 """
163
164 service = simplejson.loads(service)
165 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500166 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500167 future = simplejson.loads(future)
168 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500169 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400170 future = {}
171 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400172
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500173 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500174 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500175 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500176 resource = createResource(http, base, model, requestBuilder, developerKey,
177 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400178
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500179 def auth_method():
180 """Discovery information about the authentication the API uses."""
181 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400182
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500183 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400184
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500185 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400186
187
Joe Gregorio61d7e962011-02-22 22:52:07 -0500188def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500189 """Convert value to a string based on JSON Schema type.
190
191 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
192 JSON Schema.
193
194 Args:
195 value: any, the value to convert
196 schema_type: string, the type that value should be interpreted as
197
198 Returns:
199 A string representation of 'value' based on the schema_type.
200 """
201 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500202 if type(value) == type('') or type(value) == type(u''):
203 return value
204 else:
205 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500206 elif schema_type == 'integer':
207 return str(int(value))
208 elif schema_type == 'number':
209 return str(float(value))
210 elif schema_type == 'boolean':
211 return str(bool(value)).lower()
212 else:
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
218
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500219def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500220 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400221
222 class Resource(object):
223 """A class for interacting with a resource."""
224
225 def __init__(self):
226 self._http = http
227 self._baseUrl = baseUrl
228 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400229 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500230 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400231
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400232 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400233 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400234 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500235 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400236
Joe Gregorioca876e42011-02-22 19:39:42 -0500237 if 'parameters' not in methodDesc:
238 methodDesc['parameters'] = {}
239 for name in STACK_QUERY_PARAMETERS:
240 methodDesc['parameters'][name] = {
241 'type': 'string',
242 'restParameterType': 'query'
243 }
244
ade@google.com850cf552010-08-20 23:24:56 +0100245 if httpMethod in ['PUT', 'POST']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500246 methodDesc['parameters']['body'] = {
247 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500248 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500249 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500250 }
ade@google.com850cf552010-08-20 23:24:56 +0100251
Joe Gregorioca876e42011-02-22 19:39:42 -0500252 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100253 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500254 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100255 pattern_params = {} # Parameters that must match a regex
256 query_params = [] # Parameters that will be used in the query string
257 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500258 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500259 enum_params = {} # Allowable enumeration values for each parameter
260
261
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400262 if 'parameters' in methodDesc:
263 for arg, desc in methodDesc['parameters'].iteritems():
264 param = key2param(arg)
265 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400266
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400267 if desc.get('pattern', ''):
268 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500269 if desc.get('enum', ''):
270 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400271 if desc.get('required', False):
272 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500273 if desc.get('repeated', False):
274 repeated_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400275 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400276 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400277 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400278 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500279 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400280
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500281 for match in URITEMPLATE.finditer(pathUrl):
282 for namematch in VARNAME.finditer(match.group(0)):
283 name = key2param(namematch.group(0))
284 path_params[name] = name
285 if name in query_params:
286 query_params.remove(name)
287
Joe Gregorio48d361f2010-08-18 13:19:21 -0400288 def method(self, **kwargs):
289 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500290 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400291 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400292
ade@google.com850cf552010-08-20 23:24:56 +0100293 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400294 if name not in kwargs:
295 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400296
ade@google.com850cf552010-08-20 23:24:56 +0100297 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400298 if name in kwargs:
299 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400300 raise TypeError(
301 'Parameter "%s" value "%s" does not match the pattern "%s"' %
302 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400303
Joe Gregoriobee86832011-02-22 10:00:19 -0500304 for name, enums in enum_params.iteritems():
305 if name in kwargs:
306 if kwargs[name] not in enums:
307 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500308 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500309 (name, kwargs[name], str(enums)))
310
ade@google.com850cf552010-08-20 23:24:56 +0100311 actual_query_params = {}
312 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400313 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500314 to_type = param_type.get(key, 'string')
315 # For repeated parameters we cast each member of the list.
316 if key in repeated_params and type(value) == type([]):
317 cast_value = [_cast(x, to_type) for x in value]
318 else:
319 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100320 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500321 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100322 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500323 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100324 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400325
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400326 if self._developerKey:
327 actual_query_params['key'] = self._developerKey
328
Joe Gregorio48d361f2010-08-18 13:19:21 -0400329 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400330 headers, params, query, body = self._model.request(headers,
331 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400332
Joe Gregorioaf276d22010-12-09 14:26:58 -0500333 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
334 # document. Base URLs should not contain any path elements. If they do
335 # then urlparse.urljoin will strip them out This results in an incorrect
336 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100337 url_result = urlparse.urlsplit(self._baseUrl)
338 new_base_url = url_result.scheme + '://' + url_result.netloc
339
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400340 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500341 url = urlparse.urljoin(new_base_url,
342 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400343
ade@google.com850cf552010-08-20 23:24:56 +0100344 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500345 return self._requestBuilder(self._http,
346 self._model.response,
347 url,
348 method=httpMethod,
349 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500350 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500351 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400352
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500353 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
354 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500355 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400356 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500357 if arg in STACK_QUERY_PARAMETERS:
358 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500359 repeated = ''
360 if arg in repeated_params:
361 repeated = ' (repeated)'
362 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400363 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500364 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500365 paramdesc = methodDesc['parameters'][argmap[arg]]
366 paramdoc = paramdesc.get('description', 'A parameter')
367 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500368 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
369 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500370 enum = paramdesc.get('enum', [])
371 enumDesc = paramdesc.get('enumDescriptions', [])
372 if enum and enumDesc:
373 docs.append(' Allowed values\n')
374 for (name, desc) in zip(enum, enumDesc):
375 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400376
377 setattr(method, '__doc__', ''.join(docs))
378 setattr(theclass, methodName, method)
379
Joe Gregorioaf276d22010-12-09 14:26:58 -0500380 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
381 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400382
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500383 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400384 """
385 Takes a single argument, 'body', which is the results
386 from the last call, and returns the next set of items
387 in the collection.
388
389 Returns None if there are no more items in
390 the collection.
391 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500392 if futureDesc['type'] != 'uri':
393 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400394
395 try:
396 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500397 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400398 p = p[key]
399 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400400 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400401 return None
402
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400403 if self._developerKey:
404 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100405 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400406 q.append(('key', self._developerKey))
407 parsed[4] = urllib.urlencode(q)
408 url = urlparse.urlunparse(parsed)
409
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400410 headers = {}
411 headers, params, query, body = self._model.request(headers, {}, {}, None)
412
413 logging.info('URL being requested: %s' % url)
414 resp, content = self._http.request(url, method='GET', headers=headers)
415
Joe Gregorioabda96f2011-02-11 20:19:33 -0500416 return self._requestBuilder(self._http,
417 self._model.response,
418 url,
419 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500420 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500421 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400422
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500423 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400424
425 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400426 if 'methods' in resourceDesc:
427 for methodName, methodDesc in resourceDesc['methods'].iteritems():
428 if futureDesc:
429 future = futureDesc['methods'].get(methodName, {})
430 else:
431 future = None
432 createMethod(Resource, methodName, methodDesc, future)
433
434 # Add in nested resources
435 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500436
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500437 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400438
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500439 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400440 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500441 self._requestBuilder, self._developerKey,
442 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400443
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500444 setattr(methodResource, '__doc__', 'A collection resource.')
445 setattr(methodResource, '__is_resource__', True)
446 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400447
448 for methodName, methodDesc in resourceDesc['resources'].iteritems():
449 if futureDesc and 'resources' in futureDesc:
450 future = futureDesc['resources'].get(methodName, {})
451 else:
452 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500453 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400454
455 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500456 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400457 for methodName, methodDesc in futureDesc['methods'].iteritems():
458 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500459 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500460 resourceDesc['methods'][methodName],
461 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400462
463 return Resource()