blob: 954ad6cf9ba6455faf103e41bd2d3e2f27cab674 [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
17A client library for Google's discovery
18based APIs.
19"""
20
21__author__ = 'jcgregorio@google.com (Joe Gregorio)'
22
23
24import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010025import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040026import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040027import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040028import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040029import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040030import urlparse
ade@google.comc5eb46f2010-09-27 23:35:39 +010031try:
32 from urlparse import parse_qsl
33except ImportError:
34 from cgi import parse_qsl
Joe Gregorio5f087cf2010-09-20 16:08:07 -040035from apiclient.http import HttpRequest
Joe Gregorio48d361f2010-08-18 13:19:21 -040036
Joe Gregoriodb849af2010-09-22 16:53:59 -040037try: # pragma: no cover
Joe Gregorioe6efd532010-09-20 11:13:50 -040038 import simplejson
Joe Gregoriodb849af2010-09-22 16:53:59 -040039except ImportError: # pragma: no cover
Joe Gregorioe6efd532010-09-20 11:13:50 -040040 try:
41 # Try to import from django, should work on App Engine
42 from django.utils import simplejson
43 except ImportError:
44 # Should work for Python2.6 and higher.
45 import json as simplejson
46
Joe Gregorio48d361f2010-08-18 13:19:21 -040047
Tom Miller05cd4f52010-10-06 11:09:12 -070048class Error(Exception):
49 """Base error for this module."""
Joe Gregorio41cf7972010-08-18 15:21:06 -040050 pass
51
Joe Gregorio3bbbf662010-08-30 16:41:53 -040052
Tom Miller05cd4f52010-10-06 11:09:12 -070053class HttpError(Error):
54 """HTTP data was invalid or unexpected."""
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040055 pass
56
Tom Miller05cd4f52010-10-06 11:09:12 -070057
58class UnknownLinkType(Error):
59 """Link type unknown or unexpected."""
60 pass
61
62
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040063DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
64 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040065
66
Joe Gregorio48d361f2010-08-18 13:19:21 -040067def key2param(key):
68 """
69 max-results -> max_results
70 """
71 result = []
72 key = list(key)
73 if not key[0].isalpha():
74 result.append('x')
75 for c in key:
76 if c.isalnum():
77 result.append(c)
78 else:
79 result.append('_')
80
81 return ''.join(result)
82
83
84class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040085
ade@google.com850cf552010-08-20 23:24:56 +010086 def request(self, headers, path_params, query_params, body_value):
87 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040088 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040089 if 'user-agent' in headers:
90 headers['user-agent'] += ' '
91 else:
92 headers['user-agent'] = ''
Joe Gregorioe9369582010-09-14 15:17:28 -040093 headers['user-agent'] += 'google-api-python-client/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010094 if body_value is None:
95 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040096 else:
ade@google.com850cf552010-08-20 23:24:56 +010097 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040098 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010099 return (headers, path_params, query, simplejson.dumps(model))
100
101 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -0400102 params.update({'alt': 'json', 'prettyprint': 'true'})
103 astuples = []
104 for key, value in params.iteritems():
105 if getattr(value, 'encode', False) and callable(value.encode):
106 value = value.encode('utf-8')
107 astuples.append((key, value))
108 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400109
110 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400111 # Error handling is TBD, for example, do we retry
112 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400113 if resp.status < 300:
114 return simplejson.loads(content)['data']
115 else:
ade@google.com850cf552010-08-20 23:24:56 +0100116 logging.debug('Content from bad request was: %s' % content)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400117 if resp.get('content-type', '') != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400118 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400119 else:
120 raise HttpError(simplejson.loads(content)['error'])
121
122
Joe Gregorioc204b642010-09-21 12:01:23 -0400123def build(serviceName, version, http=None,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400124 discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400125 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400126 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400127 'apiVersion': version
128 }
ade@google.com850cf552010-08-20 23:24:56 +0100129
Joe Gregorioc204b642010-09-21 12:01:23 -0400130 if http is None:
131 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100132 requested_url = uritemplate.expand(discoveryServiceUrl, params)
133 logging.info('URL being requested: %s' % requested_url)
134 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400135 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400136 service = d['data'][serviceName][version]
137
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400138 fn = os.path.join(os.path.dirname(__file__), "contrib",
139 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400140 f = file(fn, "r")
141 d = simplejson.load(f)
142 f.close()
143 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400144 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400145
Joe Gregorio48d361f2010-08-18 13:19:21 -0400146 base = service['baseUrl']
147 resources = service['resources']
148
149 class Service(object):
150 """Top level interface for a service"""
151
152 def __init__(self, http=http):
153 self._http = http
154 self._baseUrl = base
155 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400156 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400157
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400158 def auth_discovery(self):
159 return auth_discovery
160
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400161 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400162
Joe Gregorio59cd9512010-10-04 12:46:46 -0400163 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400164 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400165 methodName, self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400166
167 setattr(method, '__doc__', 'A description of how to use this function')
168 setattr(theclass, methodName, method)
169
170 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400171 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172 return Service()
173
174
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400175def createResource(http, baseUrl, model, resourceName, developerKey,
176 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400177
178 class Resource(object):
179 """A class for interacting with a resource."""
180
181 def __init__(self):
182 self._http = http
183 self._baseUrl = baseUrl
184 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400185 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400186
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400187 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400188 pathUrl = methodDesc['pathUrl']
189 pathUrl = re.sub(r'\{', r'{+', pathUrl)
190 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400191
ade@google.com850cf552010-08-20 23:24:56 +0100192 argmap = {}
193 if httpMethod in ['PUT', 'POST']:
194 argmap['body'] = 'body'
195
196
197 required_params = [] # Required parameters
198 pattern_params = {} # Parameters that must match a regex
199 query_params = [] # Parameters that will be used in the query string
200 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400201 if 'parameters' in methodDesc:
202 for arg, desc in methodDesc['parameters'].iteritems():
203 param = key2param(arg)
204 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400205
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400206 if desc.get('pattern', ''):
207 pattern_params[param] = desc['pattern']
208 if desc.get('required', False):
209 required_params.append(param)
210 if desc.get('parameterType') == 'query':
211 query_params.append(param)
212 if desc.get('parameterType') == 'path':
213 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400214
215 def method(self, **kwargs):
216 for name in kwargs.iterkeys():
217 if name not in argmap:
218 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400219
ade@google.com850cf552010-08-20 23:24:56 +0100220 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400221 if name not in kwargs:
222 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400223
ade@google.com850cf552010-08-20 23:24:56 +0100224 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400225 if name in kwargs:
226 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400227 raise TypeError(
228 'Parameter "%s" value "%s" does not match the pattern "%s"' %
229 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400230
ade@google.com850cf552010-08-20 23:24:56 +0100231 actual_query_params = {}
232 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400233 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100234 if key in query_params:
235 actual_query_params[argmap[key]] = value
236 if key in path_params:
237 actual_path_params[argmap[key]] = value
238 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400239
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400240 if self._developerKey:
241 actual_query_params['key'] = self._developerKey
242
Joe Gregorio48d361f2010-08-18 13:19:21 -0400243 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400244 headers, params, query, body = self._model.request(headers,
245 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400246
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100247 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
248 # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
249 # This results in an incorrect URL which returns a 404
250 url_result = urlparse.urlsplit(self._baseUrl)
251 new_base_url = url_result.scheme + '://' + url_result.netloc
252
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400253 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100254 url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400255
ade@google.com850cf552010-08-20 23:24:56 +0100256 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400257 return HttpRequest(self._http, url, method=httpMethod, body=body,
258 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400259
260 docs = ['A description of how to use this function\n\n']
261 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400262 required = ""
263 if arg in required_params:
264 required = " (required)"
265 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400266
267 setattr(method, '__doc__', ''.join(docs))
268 setattr(theclass, methodName, method)
269
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400270 def createNextMethod(theclass, methodName, methodDesc):
271
272 def method(self, previous):
273 """
274 Takes a single argument, 'body', which is the results
275 from the last call, and returns the next set of items
276 in the collection.
277
278 Returns None if there are no more items in
279 the collection.
280 """
281 if methodDesc['type'] != 'uri':
282 raise UnknownLinkType(methodDesc['type'])
283
284 try:
285 p = previous
286 for key in methodDesc['location']:
287 p = p[key]
288 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400289 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400290 return None
291
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400292 if self._developerKey:
293 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100294 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400295 q.append(('key', self._developerKey))
296 parsed[4] = urllib.urlencode(q)
297 url = urlparse.urlunparse(parsed)
298
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400299 headers = {}
300 headers, params, query, body = self._model.request(headers, {}, {}, None)
301
302 logging.info('URL being requested: %s' % url)
303 resp, content = self._http.request(url, method='GET', headers=headers)
304
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400305 return HttpRequest(self._http, url, method='GET',
306 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400307
308 setattr(theclass, methodName, method)
309
310 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400311 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400312 future = futureDesc['methods'].get(methodName, {})
313 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400314
315 # Add <m>_next() methods to Resource
316 for methodName, methodDesc in futureDesc['methods'].iteritems():
317 if 'next' in methodDesc and methodName in resourceDesc['methods']:
318 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400319
320 return Resource()