blob: db042f7e750beb9c0698b6f450dce99f9eaa6315 [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 Gregoriof658e322010-10-11 15:40:31 -040036from apiclient.json import simplejson
Joe Gregorio48d361f2010-08-18 13:19:21 -040037
Tom Miller05cd4f52010-10-06 11:09:12 -070038class Error(Exception):
39 """Base error for this module."""
Joe Gregorio41cf7972010-08-18 15:21:06 -040040 pass
41
Joe Gregorio3bbbf662010-08-30 16:41:53 -040042
Tom Miller05cd4f52010-10-06 11:09:12 -070043class HttpError(Error):
44 """HTTP data was invalid or unexpected."""
Joe Gregorio0c73a672010-10-14 08:27:59 -040045 def __init__(self, resp, detail):
46 self.resp = resp
47 self.detail = detail
48 def __str__(self):
49 return self.detail
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040050
Tom Miller05cd4f52010-10-06 11:09:12 -070051
52class UnknownLinkType(Error):
53 """Link type unknown or unexpected."""
54 pass
55
56
Joe Gregorio2379ecc2010-10-26 10:51:28 -040057DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
58 '{api}/{apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040059
60
Joe Gregorio48d361f2010-08-18 13:19:21 -040061def key2param(key):
62 """
63 max-results -> max_results
64 """
65 result = []
66 key = list(key)
67 if not key[0].isalpha():
68 result.append('x')
69 for c in key:
70 if c.isalnum():
71 result.append(c)
72 else:
73 result.append('_')
74
75 return ''.join(result)
76
77
78class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040079
ade@google.com850cf552010-08-20 23:24:56 +010080 def request(self, headers, path_params, query_params, body_value):
81 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040082 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040083 if 'user-agent' in headers:
84 headers['user-agent'] += ' '
85 else:
86 headers['user-agent'] = ''
Joe Gregorioe9369582010-09-14 15:17:28 -040087 headers['user-agent'] += 'google-api-python-client/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010088 if body_value is None:
89 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040090 else:
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040091 headers['content-type'] = 'application/json'
Joe Gregorio913e70d2010-11-05 15:38:23 -040092 return (headers, path_params, query, simplejson.dumps(body_value))
ade@google.com850cf552010-08-20 23:24:56 +010093
94 def build_query(self, params):
jcgregorio@google.come3c8b6d2010-10-07 19:34:54 -040095 params.update({'alt': 'json'})
Joe Gregoriofe695fb2010-08-30 12:04:04 -040096 astuples = []
97 for key, value in params.iteritems():
98 if getattr(value, 'encode', False) and callable(value.encode):
99 value = value.encode('utf-8')
100 astuples.append((key, value))
101 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400102
103 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400104 # Error handling is TBD, for example, do we retry
105 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106 if resp.status < 300:
ade@google.com9d821b02010-10-11 03:33:51 -0700107 if resp.status == 204:
108 # A 204: No Content response should be treated differently to all the other success states
109 return simplejson.loads('{}')
Joe Gregorio78a508d2010-10-26 16:36:36 -0400110 body = simplejson.loads(content)
111 if isinstance(body, dict) and 'data' in body:
112 body = body['data']
113 return body
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114 else:
ade@google.com850cf552010-08-20 23:24:56 +0100115 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio0c73a672010-10-14 08:27:59 -0400116 if resp.get('content-type', '').startswith('application/json'):
117 raise HttpError(resp, simplejson.loads(content)['error'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400118 else:
Joe Gregorio0c73a672010-10-14 08:27:59 -0400119 raise HttpError(resp, '%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400120
121
Joe Gregorioc204b642010-09-21 12:01:23 -0400122def build(serviceName, version, http=None,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400123 discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400124 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400125 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400126 'apiVersion': version
127 }
ade@google.com850cf552010-08-20 23:24:56 +0100128
Joe Gregorioc204b642010-09-21 12:01:23 -0400129 if http is None:
130 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100131 requested_url = uritemplate.expand(discoveryServiceUrl, params)
132 logging.info('URL being requested: %s' % requested_url)
133 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400134 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400135
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400136 fn = os.path.join(os.path.dirname(__file__), "contrib",
137 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400138 try:
139 f = file(fn, "r")
140 d = simplejson.load(f)
141 f.close()
142 future = d['resources']
143 auth_discovery = d['auth']
144 except IOError:
145 future = {}
146 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400147
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400148 base = urlparse.urljoin(discoveryServiceUrl, service['restBasePath'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400149 resources = service['resources']
150
151 class Service(object):
152 """Top level interface for a service"""
153
154 def __init__(self, http=http):
155 self._http = http
156 self._baseUrl = base
157 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400158 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400159
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400160 def auth_discovery(self):
161 return auth_discovery
162
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400163 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400164
Joe Gregorio59cd9512010-10-04 12:46:46 -0400165 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400166 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400167 methodName, self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400168
169 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400170 setattr(method, '__is_resource__', True)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400171 setattr(theclass, methodName, method)
172
173 for methodName, methodDesc in resources.iteritems():
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400174 createMethod(Service, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175 return Service()
176
177
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400178def createResource(http, baseUrl, model, resourceName, developerKey,
179 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400180
181 class Resource(object):
182 """A class for interacting with a resource."""
183
184 def __init__(self):
185 self._http = http
186 self._baseUrl = baseUrl
187 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400188 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400189
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400190 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400191 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400192 pathUrl = re.sub(r'\{', r'{+', pathUrl)
193 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400194
ade@google.com850cf552010-08-20 23:24:56 +0100195 argmap = {}
196 if httpMethod in ['PUT', 'POST']:
197 argmap['body'] = 'body'
198
199
200 required_params = [] # Required parameters
201 pattern_params = {} # Parameters that must match a regex
202 query_params = [] # Parameters that will be used in the query string
203 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400204 if 'parameters' in methodDesc:
205 for arg, desc in methodDesc['parameters'].iteritems():
206 param = key2param(arg)
207 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400208
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400209 if desc.get('pattern', ''):
210 pattern_params[param] = desc['pattern']
211 if desc.get('required', False):
212 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400213 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400214 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400215 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400216 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400217
218 def method(self, **kwargs):
219 for name in kwargs.iterkeys():
220 if name not in argmap:
221 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400222
ade@google.com850cf552010-08-20 23:24:56 +0100223 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400224 if name not in kwargs:
225 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400226
ade@google.com850cf552010-08-20 23:24:56 +0100227 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400228 if name in kwargs:
229 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400230 raise TypeError(
231 'Parameter "%s" value "%s" does not match the pattern "%s"' %
232 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400233
ade@google.com850cf552010-08-20 23:24:56 +0100234 actual_query_params = {}
235 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400236 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100237 if key in query_params:
238 actual_query_params[argmap[key]] = value
239 if key in path_params:
240 actual_path_params[argmap[key]] = value
241 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400242
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400243 if self._developerKey:
244 actual_query_params['key'] = self._developerKey
245
Joe Gregorio48d361f2010-08-18 13:19:21 -0400246 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400247 headers, params, query, body = self._model.request(headers,
248 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400249
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100250 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
251 # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
252 # This results in an incorrect URL which returns a 404
253 url_result = urlparse.urlsplit(self._baseUrl)
254 new_base_url = url_result.scheme + '://' + url_result.netloc
255
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400256 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100257 url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400258
ade@google.com850cf552010-08-20 23:24:56 +0100259 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400260 return HttpRequest(self._http, url, method=httpMethod, body=body,
261 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400262
263 docs = ['A description of how to use this function\n\n']
264 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400265 required = ""
266 if arg in required_params:
267 required = " (required)"
268 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400269
270 setattr(method, '__doc__', ''.join(docs))
271 setattr(theclass, methodName, method)
272
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400273 def createNextMethod(theclass, methodName, methodDesc):
274
275 def method(self, previous):
276 """
277 Takes a single argument, 'body', which is the results
278 from the last call, and returns the next set of items
279 in the collection.
280
281 Returns None if there are no more items in
282 the collection.
283 """
284 if methodDesc['type'] != 'uri':
285 raise UnknownLinkType(methodDesc['type'])
286
287 try:
288 p = previous
289 for key in methodDesc['location']:
290 p = p[key]
291 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400292 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400293 return None
294
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400295 if self._developerKey:
296 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100297 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400298 q.append(('key', self._developerKey))
299 parsed[4] = urllib.urlencode(q)
300 url = urlparse.urlunparse(parsed)
301
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400302 headers = {}
303 headers, params, query, body = self._model.request(headers, {}, {}, None)
304
305 logging.info('URL being requested: %s' % url)
306 resp, content = self._http.request(url, method='GET', headers=headers)
307
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400308 return HttpRequest(self._http, url, method='GET',
309 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400310
311 setattr(theclass, methodName, method)
312
313 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400314 if 'methods' in resourceDesc:
315 for methodName, methodDesc in resourceDesc['methods'].iteritems():
316 if futureDesc:
317 future = futureDesc['methods'].get(methodName, {})
318 else:
319 future = None
320 createMethod(Resource, methodName, methodDesc, future)
321
322 # Add in nested resources
323 if 'resources' in resourceDesc:
324 def createMethod(theclass, methodName, methodDesc, futureDesc):
325
326 def method(self):
327 return createResource(self._http, self._baseUrl, self._model,
328 methodName, self._developerKey, methodDesc, futureDesc)
329
330 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400331 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400332 setattr(theclass, methodName, method)
333
334 for methodName, methodDesc in resourceDesc['resources'].iteritems():
335 if futureDesc and 'resources' in futureDesc:
336 future = futureDesc['resources'].get(methodName, {})
337 else:
338 future = {}
339 createMethod(Resource, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400340
341 # Add <m>_next() methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400342 if futureDesc:
343 for methodName, methodDesc in futureDesc['methods'].iteritems():
344 if 'next' in methodDesc and methodName in resourceDesc['methods']:
345 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400346
347 return Resource()