blob: bddcef1083d3de0a502bdb623f97b503fbee9779 [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 Gregorio2379ecc2010-10-26 10:51:28 -040044DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
45 '{api}/{apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040046
47
Joe Gregorio48d361f2010-08-18 13:19:21 -040048def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050049 """Converts key names into parameter names.
50
51 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -040052 """
53 result = []
54 key = list(key)
55 if not key[0].isalpha():
56 result.append('x')
57 for c in key:
58 if c.isalnum():
59 result.append(c)
60 else:
61 result.append('_')
62
63 return ''.join(result)
64
65
Joe Gregorioaf276d22010-12-09 14:26:58 -050066def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050067 http=None,
68 discoveryServiceUrl=DISCOVERY_URI,
69 developerKey=None,
70 model=JsonModel(),
71 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050072 """Construct a Resource for interacting with an API.
73
74 Construct a Resource object for interacting with
75 an API. The serviceName and version are the
76 names from the Discovery service.
77
78 Args:
79 serviceName: string, name of the service
80 version: string, the version of the service
81 discoveryServiceUrl: string, a URI Template that points to
82 the location of the discovery service. It should have two
83 parameters {api} and {apiVersion} that when filled in
84 produce an absolute URI to the discovery document for
85 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050086 developerKey: string, key obtained
87 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -050088 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -050089 requestBuilder: apiclient.http.HttpRequest, encapsulator for
90 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -050091
92 Returns:
93 A Resource object with methods for interacting with
94 the service.
95 """
Joe Gregorio48d361f2010-08-18 13:19:21 -040096 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040097 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -040098 'apiVersion': version
99 }
ade@google.com850cf552010-08-20 23:24:56 +0100100
Joe Gregorioc204b642010-09-21 12:01:23 -0400101 if http is None:
102 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100103 requested_url = uritemplate.expand(discoveryServiceUrl, params)
104 logging.info('URL being requested: %s' % requested_url)
105 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400106 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400107
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500108 fn = os.path.join(os.path.dirname(__file__), 'contrib',
109 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400110 try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500111 f = file(fn, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500112 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400113 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400114 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500115 future = None
116
117 return build_from_document(content, discoveryServiceUrl, future,
118 http, developerKey, model, requestBuilder)
119
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500120
Joe Gregorio292b9b82011-01-12 11:36:11 -0500121def build_from_document(
122 service,
123 base,
124 future=None,
125 http=None,
126 developerKey=None,
127 model=JsonModel(),
128 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500129 """Create a Resource for interacting with an API.
130
131 Same as `build()`, but constructs the Resource object
132 from a discovery document that is it given, as opposed to
133 retrieving one over HTTP.
134
Joe Gregorio292b9b82011-01-12 11:36:11 -0500135 Args:
136 service: string, discovery document
137 base: string, base URI for all HTTP requests, usually the discovery URI
138 future: string, discovery document with future capabilities
139 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500140 http: httplib2.Http, An instance of httplib2.Http or something that acts
141 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500142 developerKey: string, Key for controlling API usage, generated
143 from the API Console.
144 model: Model class instance that serializes and
145 de-serializes requests and responses.
146 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500147
148 Returns:
149 A Resource object with methods for interacting with
150 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500151 """
152
153 service = simplejson.loads(service)
154 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500155 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500156 future = simplejson.loads(future)
157 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500158 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400159 future = {}
160 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400161
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500162 resource = createResource(http, base, model, requestBuilder, developerKey,
163 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400164
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500165 def auth_method():
166 """Discovery information about the authentication the API uses."""
167 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400168
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500169 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400170
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500171 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172
173
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500174def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500175 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400176
177 class Resource(object):
178 """A class for interacting with a resource."""
179
180 def __init__(self):
181 self._http = http
182 self._baseUrl = baseUrl
183 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400184 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500185 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400186
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400187 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400188 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400189 pathUrl = re.sub(r'\{', r'{+', pathUrl)
190 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500191 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400192
ade@google.com850cf552010-08-20 23:24:56 +0100193 argmap = {}
194 if httpMethod in ['PUT', 'POST']:
195 argmap['body'] = 'body'
196
197
198 required_params = [] # Required parameters
199 pattern_params = {} # Parameters that must match a regex
200 query_params = [] # Parameters that will be used in the query string
201 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400202 if 'parameters' in methodDesc:
203 for arg, desc in methodDesc['parameters'].iteritems():
204 param = key2param(arg)
205 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400206
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400207 if desc.get('pattern', ''):
208 pattern_params[param] = desc['pattern']
209 if desc.get('required', False):
210 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400211 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400212 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400213 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400214 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400215
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500216 for match in URITEMPLATE.finditer(pathUrl):
217 for namematch in VARNAME.finditer(match.group(0)):
218 name = key2param(namematch.group(0))
219 path_params[name] = name
220 if name in query_params:
221 query_params.remove(name)
222
Joe Gregorio48d361f2010-08-18 13:19:21 -0400223 def method(self, **kwargs):
224 for name in kwargs.iterkeys():
225 if name not in argmap:
226 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400227
ade@google.com850cf552010-08-20 23:24:56 +0100228 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400229 if name not in kwargs:
230 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400231
ade@google.com850cf552010-08-20 23:24:56 +0100232 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400233 if name in kwargs:
234 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400235 raise TypeError(
236 'Parameter "%s" value "%s" does not match the pattern "%s"' %
237 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400238
ade@google.com850cf552010-08-20 23:24:56 +0100239 actual_query_params = {}
240 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400241 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100242 if key in query_params:
243 actual_query_params[argmap[key]] = value
244 if key in path_params:
245 actual_path_params[argmap[key]] = value
246 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400247
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400248 if self._developerKey:
249 actual_query_params['key'] = self._developerKey
250
Joe Gregorio48d361f2010-08-18 13:19:21 -0400251 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400252 headers, params, query, body = self._model.request(headers,
253 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400254
Joe Gregorioaf276d22010-12-09 14:26:58 -0500255 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
256 # document. Base URLs should not contain any path elements. If they do
257 # then urlparse.urljoin will strip them out This results in an incorrect
258 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100259 url_result = urlparse.urlsplit(self._baseUrl)
260 new_base_url = url_result.scheme + '://' + url_result.netloc
261
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400262 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500263 url = urlparse.urljoin(new_base_url,
264 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400265
ade@google.com850cf552010-08-20 23:24:56 +0100266 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500267 return self._requestBuilder(self._http,
268 self._model.response,
269 url,
270 method=httpMethod,
271 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500272 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500273 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400274
275 docs = ['A description of how to use this function\n\n']
276 for arg in argmap.iterkeys():
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500277 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400278 if arg in required_params:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500279 required = ' (required)'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400280 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400281
282 setattr(method, '__doc__', ''.join(docs))
283 setattr(theclass, methodName, method)
284
Joe Gregorioaf276d22010-12-09 14:26:58 -0500285 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
286 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400287
288 def method(self, previous):
289 """
290 Takes a single argument, 'body', which is the results
291 from the last call, and returns the next set of items
292 in the collection.
293
294 Returns None if there are no more items in
295 the collection.
296 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500297 if futureDesc['type'] != 'uri':
298 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400299
300 try:
301 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500302 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400303 p = p[key]
304 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400305 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400306 return None
307
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400308 if self._developerKey:
309 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100310 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400311 q.append(('key', self._developerKey))
312 parsed[4] = urllib.urlencode(q)
313 url = urlparse.urlunparse(parsed)
314
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400315 headers = {}
316 headers, params, query, body = self._model.request(headers, {}, {}, None)
317
318 logging.info('URL being requested: %s' % url)
319 resp, content = self._http.request(url, method='GET', headers=headers)
320
Joe Gregorioabda96f2011-02-11 20:19:33 -0500321 return self._requestBuilder(self._http,
322 self._model.response,
323 url,
324 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500325 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500326 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400327
328 setattr(theclass, methodName, method)
329
330 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400331 if 'methods' in resourceDesc:
332 for methodName, methodDesc in resourceDesc['methods'].iteritems():
333 if futureDesc:
334 future = futureDesc['methods'].get(methodName, {})
335 else:
336 future = None
337 createMethod(Resource, methodName, methodDesc, future)
338
339 # Add in nested resources
340 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500341
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400342 def createMethod(theclass, methodName, methodDesc, futureDesc):
343
344 def method(self):
345 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500346 self._requestBuilder, self._developerKey,
347 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400348
349 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400350 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400351 setattr(theclass, methodName, method)
352
353 for methodName, methodDesc in resourceDesc['resources'].iteritems():
354 if futureDesc and 'resources' in futureDesc:
355 future = futureDesc['resources'].get(methodName, {})
356 else:
357 future = {}
Joe Gregorioaf276d22010-12-09 14:26:58 -0500358 createMethod(Resource, methodName, methodDesc,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500359 future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400360
361 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500362 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400363 for methodName, methodDesc in futureDesc['methods'].iteritems():
364 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500365 createNextMethod(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500366 resourceDesc['methods'][methodName],
367 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400368
369 return Resource()