blob: a5b7c9a0bf7ef1fb36691b79078576698745f45d [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 Gregorio46b0ff62010-10-09 22:13:12 -040091 if len(body_value) == 1 and 'data' in body_value:
92 model = body_value
93 else:
94 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040095 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010096 return (headers, path_params, query, simplejson.dumps(model))
97
98 def build_query(self, params):
jcgregorio@google.come3c8b6d2010-10-07 19:34:54 -040099 params.update({'alt': 'json'})
Joe Gregoriofe695fb2010-08-30 12:04:04 -0400100 astuples = []
101 for key, value in params.iteritems():
102 if getattr(value, 'encode', False) and callable(value.encode):
103 value = value.encode('utf-8')
104 astuples.append((key, value))
105 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106
107 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400108 # Error handling is TBD, for example, do we retry
109 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400110 if resp.status < 300:
ade@google.com9d821b02010-10-11 03:33:51 -0700111 if resp.status == 204:
112 # A 204: No Content response should be treated differently to all the other success states
113 return simplejson.loads('{}')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114 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 Gregorio0c73a672010-10-14 08:27:59 -0400117 if resp.get('content-type', '').startswith('application/json'):
118 raise HttpError(resp, simplejson.loads(content)['error'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400119 else:
Joe Gregorio0c73a672010-10-14 08:27:59 -0400120 raise HttpError(resp, '%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400121
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 Gregorio2379ecc2010-10-26 10:51:28 -0400135 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400136
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400137 fn = os.path.join(os.path.dirname(__file__), "contrib",
138 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400139 try:
140 f = file(fn, "r")
141 d = simplejson.load(f)
142 f.close()
143 future = d['resources']
144 auth_discovery = d['auth']
145 except IOError:
146 future = {}
147 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400148
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400149 base = urlparse.urljoin(discoveryServiceUrl, service['restBasePath'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400150 resources = service['resources']
151
152 class Service(object):
153 """Top level interface for a service"""
154
155 def __init__(self, http=http):
156 self._http = http
157 self._baseUrl = base
158 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400159 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400160
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400161 def auth_discovery(self):
162 return auth_discovery
163
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400164 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400165
Joe Gregorio59cd9512010-10-04 12:46:46 -0400166 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400167 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400168 methodName, self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400169
170 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400171 setattr(method, '__is_resource__', True)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172 setattr(theclass, methodName, method)
173
174 for methodName, methodDesc in resources.iteritems():
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400175 createMethod(Service, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400176 return Service()
177
178
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400179def createResource(http, baseUrl, model, resourceName, developerKey,
180 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400181
182 class Resource(object):
183 """A class for interacting with a resource."""
184
185 def __init__(self):
186 self._http = http
187 self._baseUrl = baseUrl
188 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400189 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400190
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400191 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400192 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400193 pathUrl = re.sub(r'\{', r'{+', pathUrl)
194 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400195
ade@google.com850cf552010-08-20 23:24:56 +0100196 argmap = {}
197 if httpMethod in ['PUT', 'POST']:
198 argmap['body'] = 'body'
199
200
201 required_params = [] # Required parameters
202 pattern_params = {} # Parameters that must match a regex
203 query_params = [] # Parameters that will be used in the query string
204 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400205 if 'parameters' in methodDesc:
206 for arg, desc in methodDesc['parameters'].iteritems():
207 param = key2param(arg)
208 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400209
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400210 if desc.get('pattern', ''):
211 pattern_params[param] = desc['pattern']
212 if desc.get('required', False):
213 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400214 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400215 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400216 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400217 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400218
219 def method(self, **kwargs):
220 for name in kwargs.iterkeys():
221 if name not in argmap:
222 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400223
ade@google.com850cf552010-08-20 23:24:56 +0100224 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400225 if name not in kwargs:
226 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400227
ade@google.com850cf552010-08-20 23:24:56 +0100228 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400229 if name in kwargs:
230 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400231 raise TypeError(
232 'Parameter "%s" value "%s" does not match the pattern "%s"' %
233 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400234
ade@google.com850cf552010-08-20 23:24:56 +0100235 actual_query_params = {}
236 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400237 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100238 if key in query_params:
239 actual_query_params[argmap[key]] = value
240 if key in path_params:
241 actual_path_params[argmap[key]] = value
242 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400243
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400244 if self._developerKey:
245 actual_query_params['key'] = self._developerKey
246
Joe Gregorio48d361f2010-08-18 13:19:21 -0400247 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400248 headers, params, query, body = self._model.request(headers,
249 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400250
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100251 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
252 # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
253 # This results in an incorrect URL which returns a 404
254 url_result = urlparse.urlsplit(self._baseUrl)
255 new_base_url = url_result.scheme + '://' + url_result.netloc
256
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400257 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100258 url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400259
ade@google.com850cf552010-08-20 23:24:56 +0100260 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400261 return HttpRequest(self._http, url, method=httpMethod, body=body,
262 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400263
264 docs = ['A description of how to use this function\n\n']
265 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400266 required = ""
267 if arg in required_params:
268 required = " (required)"
269 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400270
271 setattr(method, '__doc__', ''.join(docs))
272 setattr(theclass, methodName, method)
273
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400274 def createNextMethod(theclass, methodName, methodDesc):
275
276 def method(self, previous):
277 """
278 Takes a single argument, 'body', which is the results
279 from the last call, and returns the next set of items
280 in the collection.
281
282 Returns None if there are no more items in
283 the collection.
284 """
285 if methodDesc['type'] != 'uri':
286 raise UnknownLinkType(methodDesc['type'])
287
288 try:
289 p = previous
290 for key in methodDesc['location']:
291 p = p[key]
292 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400293 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400294 return None
295
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400296 if self._developerKey:
297 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100298 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400299 q.append(('key', self._developerKey))
300 parsed[4] = urllib.urlencode(q)
301 url = urlparse.urlunparse(parsed)
302
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400303 headers = {}
304 headers, params, query, body = self._model.request(headers, {}, {}, None)
305
306 logging.info('URL being requested: %s' % url)
307 resp, content = self._http.request(url, method='GET', headers=headers)
308
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400309 return HttpRequest(self._http, url, method='GET',
310 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400311
312 setattr(theclass, methodName, method)
313
314 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400315 if 'methods' in resourceDesc:
316 for methodName, methodDesc in resourceDesc['methods'].iteritems():
317 if futureDesc:
318 future = futureDesc['methods'].get(methodName, {})
319 else:
320 future = None
321 createMethod(Resource, methodName, methodDesc, future)
322
323 # Add in nested resources
324 if 'resources' in resourceDesc:
325 def createMethod(theclass, methodName, methodDesc, futureDesc):
326
327 def method(self):
328 return createResource(self._http, self._baseUrl, self._model,
329 methodName, self._developerKey, methodDesc, futureDesc)
330
331 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400332 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400333 setattr(theclass, methodName, method)
334
335 for methodName, methodDesc in resourceDesc['resources'].iteritems():
336 if futureDesc and 'resources' in futureDesc:
337 future = futureDesc['resources'].get(methodName, {})
338 else:
339 future = {}
340 createMethod(Resource, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400341
342 # Add <m>_next() methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400343 if futureDesc:
344 for methodName, methodDesc in futureDesc['methods'].iteritems():
345 if 'next' in methodDesc and methodName in resourceDesc['methods']:
346 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400347
348 return Resource()