blob: 945db74d6142a8be0ce10bcd7a8648b34b29a694 [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:
Joe Gregorio46b0ff62010-10-09 22:13:12 -040097 if len(body_value) == 1 and 'data' in body_value:
98 model = body_value
99 else:
100 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -0400101 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +0100102 return (headers, path_params, query, simplejson.dumps(model))
103
104 def build_query(self, params):
jcgregorio@google.come3c8b6d2010-10-07 19:34:54 -0400105 params.update({'alt': 'json'})
Joe Gregoriofe695fb2010-08-30 12:04:04 -0400106 astuples = []
107 for key, value in params.iteritems():
108 if getattr(value, 'encode', False) and callable(value.encode):
109 value = value.encode('utf-8')
110 astuples.append((key, value))
111 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400112
113 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400114 # Error handling is TBD, for example, do we retry
115 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400116 if resp.status < 300:
ade@google.com9d821b02010-10-11 03:33:51 -0700117 if resp.status == 204:
118 # A 204: No Content response should be treated differently to all the other success states
119 return simplejson.loads('{}')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400120 return simplejson.loads(content)['data']
121 else:
ade@google.com850cf552010-08-20 23:24:56 +0100122 logging.debug('Content from bad request was: %s' % content)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400123 if resp.get('content-type', '') != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400124 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400125 else:
126 raise HttpError(simplejson.loads(content)['error'])
127
128
Joe Gregorioc204b642010-09-21 12:01:23 -0400129def build(serviceName, version, http=None,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400130 discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400131 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400132 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400133 'apiVersion': version
134 }
ade@google.com850cf552010-08-20 23:24:56 +0100135
Joe Gregorioc204b642010-09-21 12:01:23 -0400136 if http is None:
137 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100138 requested_url = uritemplate.expand(discoveryServiceUrl, params)
139 logging.info('URL being requested: %s' % requested_url)
140 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400141 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400142 service = d['data'][serviceName][version]
143
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400144 fn = os.path.join(os.path.dirname(__file__), "contrib",
145 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400146 f = file(fn, "r")
147 d = simplejson.load(f)
148 f.close()
149 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400150 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400151
Joe Gregorio48d361f2010-08-18 13:19:21 -0400152 base = service['baseUrl']
153 resources = service['resources']
154
155 class Service(object):
156 """Top level interface for a service"""
157
158 def __init__(self, http=http):
159 self._http = http
160 self._baseUrl = base
161 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400162 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400163
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400164 def auth_discovery(self):
165 return auth_discovery
166
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400167 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400168
Joe Gregorio59cd9512010-10-04 12:46:46 -0400169 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400170 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400171 methodName, self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172
173 setattr(method, '__doc__', 'A description of how to use this function')
174 setattr(theclass, methodName, method)
175
176 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400177 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400178 return Service()
179
180
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400181def createResource(http, baseUrl, model, resourceName, developerKey,
182 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400183
184 class Resource(object):
185 """A class for interacting with a resource."""
186
187 def __init__(self):
188 self._http = http
189 self._baseUrl = baseUrl
190 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400191 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400192
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400193 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400194 pathUrl = methodDesc['pathUrl']
195 pathUrl = re.sub(r'\{', r'{+', pathUrl)
196 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400197
ade@google.com850cf552010-08-20 23:24:56 +0100198 argmap = {}
199 if httpMethod in ['PUT', 'POST']:
200 argmap['body'] = 'body'
201
202
203 required_params = [] # Required parameters
204 pattern_params = {} # Parameters that must match a regex
205 query_params = [] # Parameters that will be used in the query string
206 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400207 if 'parameters' in methodDesc:
208 for arg, desc in methodDesc['parameters'].iteritems():
209 param = key2param(arg)
210 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400211
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400212 if desc.get('pattern', ''):
213 pattern_params[param] = desc['pattern']
214 if desc.get('required', False):
215 required_params.append(param)
216 if desc.get('parameterType') == 'query':
217 query_params.append(param)
218 if desc.get('parameterType') == 'path':
219 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400220
221 def method(self, **kwargs):
222 for name in kwargs.iterkeys():
223 if name not in argmap:
224 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400225
ade@google.com850cf552010-08-20 23:24:56 +0100226 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400227 if name not in kwargs:
228 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400229
ade@google.com850cf552010-08-20 23:24:56 +0100230 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400231 if name in kwargs:
232 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400233 raise TypeError(
234 'Parameter "%s" value "%s" does not match the pattern "%s"' %
235 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400236
ade@google.com850cf552010-08-20 23:24:56 +0100237 actual_query_params = {}
238 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400239 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100240 if key in query_params:
241 actual_query_params[argmap[key]] = value
242 if key in path_params:
243 actual_path_params[argmap[key]] = value
244 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400245
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400246 if self._developerKey:
247 actual_query_params['key'] = self._developerKey
248
Joe Gregorio48d361f2010-08-18 13:19:21 -0400249 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400250 headers, params, query, body = self._model.request(headers,
251 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400252
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100253 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
254 # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
255 # This results in an incorrect URL which returns a 404
256 url_result = urlparse.urlsplit(self._baseUrl)
257 new_base_url = url_result.scheme + '://' + url_result.netloc
258
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400259 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100260 url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400261
ade@google.com850cf552010-08-20 23:24:56 +0100262 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400263 return HttpRequest(self._http, url, method=httpMethod, body=body,
264 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400265
266 docs = ['A description of how to use this function\n\n']
267 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400268 required = ""
269 if arg in required_params:
270 required = " (required)"
271 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400272
273 setattr(method, '__doc__', ''.join(docs))
274 setattr(theclass, methodName, method)
275
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400276 def createNextMethod(theclass, methodName, methodDesc):
277
278 def method(self, previous):
279 """
280 Takes a single argument, 'body', which is the results
281 from the last call, and returns the next set of items
282 in the collection.
283
284 Returns None if there are no more items in
285 the collection.
286 """
287 if methodDesc['type'] != 'uri':
288 raise UnknownLinkType(methodDesc['type'])
289
290 try:
291 p = previous
292 for key in methodDesc['location']:
293 p = p[key]
294 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400295 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400296 return None
297
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400298 if self._developerKey:
299 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100300 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400301 q.append(('key', self._developerKey))
302 parsed[4] = urllib.urlencode(q)
303 url = urlparse.urlunparse(parsed)
304
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400305 headers = {}
306 headers, params, query, body = self._model.request(headers, {}, {}, None)
307
308 logging.info('URL being requested: %s' % url)
309 resp, content = self._http.request(url, method='GET', headers=headers)
310
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400311 return HttpRequest(self._http, url, method='GET',
312 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400313
314 setattr(theclass, methodName, method)
315
316 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400317 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400318 future = futureDesc['methods'].get(methodName, {})
319 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400320
321 # Add <m>_next() methods to Resource
322 for methodName, methodDesc in futureDesc['methods'].iteritems():
323 if 'next' in methodDesc and methodName in resourceDesc['methods']:
324 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400325
326 return Resource()