blob: 0c4c8547e778ec95133b1289644c293b92f7aaf1 [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 Gregorio48d361f2010-08-18 13:19:21 -040042
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050043URITEMPLATE = re.compile('{[^}]*}')
44VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050045DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.3/describe/'
Joe Gregorio2379ecc2010-10-26 10:51:28 -040046 '{api}/{apiVersion}')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050047DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050048
49# Query parameters that work, but don't appear in discovery
Joe Gregorio13217952011-02-22 15:37:38 -050050STACK_QUERY_PARAMETERS = ['trace']
Joe Gregorio48d361f2010-08-18 13:19:21 -040051
52
Joe Gregorio48d361f2010-08-18 13:19:21 -040053def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050054 """Converts key names into parameter names.
55
56 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040057 """
58 result = []
59 key = list(key)
60 if not key[0].isalpha():
61 result.append('x')
62 for c in key:
63 if c.isalnum():
64 result.append(c)
65 else:
66 result.append('_')
67
68 return ''.join(result)
69
70
Joe Gregorioaf276d22010-12-09 14:26:58 -050071def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050072 http=None,
73 discoveryServiceUrl=DISCOVERY_URI,
74 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -050075 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -050076 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050077 """Construct a Resource for interacting with an API.
78
79 Construct a Resource object for interacting with
80 an API. The serviceName and version are the
81 names from the Discovery service.
82
83 Args:
84 serviceName: string, name of the service
85 version: string, the version of the service
86 discoveryServiceUrl: string, a URI Template that points to
87 the location of the discovery service. It should have two
88 parameters {api} and {apiVersion} that when filled in
89 produce an absolute URI to the discovery document for
90 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050091 developerKey: string, key obtained
92 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -050093 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -050094 requestBuilder: apiclient.http.HttpRequest, encapsulator for
95 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -050096
97 Returns:
98 A Resource object with methods for interacting with
99 the service.
100 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400101 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400102 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400103 'apiVersion': version
104 }
ade@google.com850cf552010-08-20 23:24:56 +0100105
Joe Gregorioc204b642010-09-21 12:01:23 -0400106 if http is None:
107 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100108 requested_url = uritemplate.expand(discoveryServiceUrl, params)
109 logging.info('URL being requested: %s' % requested_url)
110 resp, content = http.request(requested_url)
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500111 try:
112 service = simplejson.loads(content)
113 except ValueError, e:
114 raise HttpError(resp, content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400115
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500116 fn = os.path.join(os.path.dirname(__file__), 'contrib',
117 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400118 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500119 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500120 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400121 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400122 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500123 future = None
124
125 return build_from_document(content, discoveryServiceUrl, future,
126 http, developerKey, model, requestBuilder)
127
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500128
Joe Gregorio292b9b82011-01-12 11:36:11 -0500129def build_from_document(
130 service,
131 base,
132 future=None,
133 http=None,
134 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500135 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500136 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500137 """Create a Resource for interacting with an API.
138
139 Same as `build()`, but constructs the Resource object
140 from a discovery document that is it given, as opposed to
141 retrieving one over HTTP.
142
Joe Gregorio292b9b82011-01-12 11:36:11 -0500143 Args:
144 service: string, discovery document
145 base: string, base URI for all HTTP requests, usually the discovery URI
146 future: string, discovery document with future capabilities
147 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500148 http: httplib2.Http, An instance of httplib2.Http or something that acts
149 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500150 developerKey: string, Key for controlling API usage, generated
151 from the API Console.
152 model: Model class instance that serializes and
153 de-serializes requests and responses.
154 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500155
156 Returns:
157 A Resource object with methods for interacting with
158 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500159 """
160
161 service = simplejson.loads(service)
162 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500163 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500164 future = simplejson.loads(future)
165 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500166 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400167 future = {}
168 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400169
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500170 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500171 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500172 model = JsonModel('dataWrapper' in features)
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500173 resource = createResource(http, base, model, requestBuilder, developerKey,
174 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500176 def auth_method():
177 """Discovery information about the authentication the API uses."""
178 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400179
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500180 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400181
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500182 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400183
184
Joe Gregorio61d7e962011-02-22 22:52:07 -0500185def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500186 """Convert value to a string based on JSON Schema type.
187
188 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
189 JSON Schema.
190
191 Args:
192 value: any, the value to convert
193 schema_type: string, the type that value should be interpreted as
194
195 Returns:
196 A string representation of 'value' based on the schema_type.
197 """
198 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500199 if type(value) == type('') or type(value) == type(u''):
200 return value
201 else:
202 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500203 elif schema_type == 'integer':
204 return str(int(value))
205 elif schema_type == 'number':
206 return str(float(value))
207 elif schema_type == 'boolean':
208 return str(bool(value)).lower()
209 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500210 if type(value) == type('') or type(value) == type(u''):
211 return value
212 else:
213 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500214
215
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500216def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500217 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400218
219 class Resource(object):
220 """A class for interacting with a resource."""
221
222 def __init__(self):
223 self._http = http
224 self._baseUrl = baseUrl
225 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400226 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500227 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400228
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400229 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400230 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400231 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500232 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400233
Joe Gregorioca876e42011-02-22 19:39:42 -0500234 if 'parameters' not in methodDesc:
235 methodDesc['parameters'] = {}
236 for name in STACK_QUERY_PARAMETERS:
237 methodDesc['parameters'][name] = {
238 'type': 'string',
239 'restParameterType': 'query'
240 }
241
ade@google.com850cf552010-08-20 23:24:56 +0100242 if httpMethod in ['PUT', 'POST']:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500243 methodDesc['parameters']['body'] = {
244 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500245 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500246 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500247 }
ade@google.com850cf552010-08-20 23:24:56 +0100248
Joe Gregorioca876e42011-02-22 19:39:42 -0500249 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100250 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500251 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100252 pattern_params = {} # Parameters that must match a regex
253 query_params = [] # Parameters that will be used in the query string
254 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500255 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500256 enum_params = {} # Allowable enumeration values for each parameter
257
258
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400259 if 'parameters' in methodDesc:
260 for arg, desc in methodDesc['parameters'].iteritems():
261 param = key2param(arg)
262 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400263
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400264 if desc.get('pattern', ''):
265 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500266 if desc.get('enum', ''):
267 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400268 if desc.get('required', False):
269 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500270 if desc.get('repeated', False):
271 repeated_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400272 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400273 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400274 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400275 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500276 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400277
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500278 for match in URITEMPLATE.finditer(pathUrl):
279 for namematch in VARNAME.finditer(match.group(0)):
280 name = key2param(namematch.group(0))
281 path_params[name] = name
282 if name in query_params:
283 query_params.remove(name)
284
Joe Gregorio48d361f2010-08-18 13:19:21 -0400285 def method(self, **kwargs):
286 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500287 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400288 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400289
ade@google.com850cf552010-08-20 23:24:56 +0100290 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400291 if name not in kwargs:
292 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400293
ade@google.com850cf552010-08-20 23:24:56 +0100294 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400295 if name in kwargs:
296 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400297 raise TypeError(
298 'Parameter "%s" value "%s" does not match the pattern "%s"' %
299 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400300
Joe Gregoriobee86832011-02-22 10:00:19 -0500301 for name, enums in enum_params.iteritems():
302 if name in kwargs:
303 if kwargs[name] not in enums:
304 raise TypeError(
Joe Gregorioca876e42011-02-22 19:39:42 -0500305 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
Joe Gregoriobee86832011-02-22 10:00:19 -0500306 (name, kwargs[name], str(enums)))
307
ade@google.com850cf552010-08-20 23:24:56 +0100308 actual_query_params = {}
309 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400310 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500311 to_type = param_type.get(key, 'string')
312 # For repeated parameters we cast each member of the list.
313 if key in repeated_params and type(value) == type([]):
314 cast_value = [_cast(x, to_type) for x in value]
315 else:
316 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100317 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500318 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100319 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500320 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100321 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400322
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400323 if self._developerKey:
324 actual_query_params['key'] = self._developerKey
325
Joe Gregorio48d361f2010-08-18 13:19:21 -0400326 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400327 headers, params, query, body = self._model.request(headers,
328 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400329
Joe Gregorioaf276d22010-12-09 14:26:58 -0500330 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
331 # document. Base URLs should not contain any path elements. If they do
332 # then urlparse.urljoin will strip them out This results in an incorrect
333 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100334 url_result = urlparse.urlsplit(self._baseUrl)
335 new_base_url = url_result.scheme + '://' + url_result.netloc
336
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400337 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500338 url = urlparse.urljoin(new_base_url,
339 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400340
ade@google.com850cf552010-08-20 23:24:56 +0100341 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500342 return self._requestBuilder(self._http,
343 self._model.response,
344 url,
345 method=httpMethod,
346 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500347 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500348 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400349
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500350 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
351 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500352 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400353 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500354 if arg in STACK_QUERY_PARAMETERS:
355 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500356 repeated = ''
357 if arg in repeated_params:
358 repeated = ' (repeated)'
359 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400360 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500361 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500362 paramdesc = methodDesc['parameters'][argmap[arg]]
363 paramdoc = paramdesc.get('description', 'A parameter')
364 paramtype = paramdesc.get('type', 'string')
Joe Gregorio61d7e962011-02-22 22:52:07 -0500365 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
366 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500367 enum = paramdesc.get('enum', [])
368 enumDesc = paramdesc.get('enumDescriptions', [])
369 if enum and enumDesc:
370 docs.append(' Allowed values\n')
371 for (name, desc) in zip(enum, enumDesc):
372 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400373
374 setattr(method, '__doc__', ''.join(docs))
375 setattr(theclass, methodName, method)
376
Joe Gregorioaf276d22010-12-09 14:26:58 -0500377 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
378 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400379
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500380 def methodNext(self, previous):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400381 """
382 Takes a single argument, 'body', which is the results
383 from the last call, and returns the next set of items
384 in the collection.
385
386 Returns None if there are no more items in
387 the collection.
388 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500389 if futureDesc['type'] != 'uri':
390 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400391
392 try:
393 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500394 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400395 p = p[key]
396 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400397 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400398 return None
399
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400400 if self._developerKey:
401 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100402 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400403 q.append(('key', self._developerKey))
404 parsed[4] = urllib.urlencode(q)
405 url = urlparse.urlunparse(parsed)
406
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400407 headers = {}
408 headers, params, query, body = self._model.request(headers, {}, {}, None)
409
410 logging.info('URL being requested: %s' % url)
411 resp, content = self._http.request(url, method='GET', headers=headers)
412
Joe Gregorioabda96f2011-02-11 20:19:33 -0500413 return self._requestBuilder(self._http,
414 self._model.response,
415 url,
416 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500417 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500418 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400419
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500420 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400421
422 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400423 if 'methods' in resourceDesc:
424 for methodName, methodDesc in resourceDesc['methods'].iteritems():
425 if futureDesc:
426 future = futureDesc['methods'].get(methodName, {})
427 else:
428 future = None
429 createMethod(Resource, methodName, methodDesc, future)
430
431 # Add in nested resources
432 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500433
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500434 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400435
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500436 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400437 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500438 self._requestBuilder, self._developerKey,
439 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400440
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500441 setattr(methodResource, '__doc__', 'A collection resource.')
442 setattr(methodResource, '__is_resource__', True)
443 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400444
445 for methodName, methodDesc in resourceDesc['resources'].iteritems():
446 if futureDesc and 'resources' in futureDesc:
447 future = futureDesc['resources'].get(methodName, {})
448 else:
449 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500450 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400451
452 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500453 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400454 for methodName, methodDesc in futureDesc['methods'].iteritems():
455 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500456 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500457 resourceDesc['methods'][methodName],
458 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400459
460 return Resource()