blob: fe98db1e0885908f13c0116b68b050e96b2993f2 [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 Gregorio78a508d2010-10-26 16:36:36 -0400114 body = simplejson.loads(content)
115 if isinstance(body, dict) and 'data' in body:
116 body = body['data']
117 return body
Joe Gregorio48d361f2010-08-18 13:19:21 -0400118 else:
ade@google.com850cf552010-08-20 23:24:56 +0100119 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio0c73a672010-10-14 08:27:59 -0400120 if resp.get('content-type', '').startswith('application/json'):
121 raise HttpError(resp, simplejson.loads(content)['error'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400122 else:
Joe Gregorio0c73a672010-10-14 08:27:59 -0400123 raise HttpError(resp, '%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400124
125
Joe Gregorioc204b642010-09-21 12:01:23 -0400126def build(serviceName, version, http=None,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400127 discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400128 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400129 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400130 'apiVersion': version
131 }
ade@google.com850cf552010-08-20 23:24:56 +0100132
Joe Gregorioc204b642010-09-21 12:01:23 -0400133 if http is None:
134 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100135 requested_url = uritemplate.expand(discoveryServiceUrl, params)
136 logging.info('URL being requested: %s' % requested_url)
137 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400138 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400139
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400140 fn = os.path.join(os.path.dirname(__file__), "contrib",
141 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400142 try:
143 f = file(fn, "r")
144 d = simplejson.load(f)
145 f.close()
146 future = d['resources']
147 auth_discovery = d['auth']
148 except IOError:
149 future = {}
150 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400151
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400152 base = urlparse.urljoin(discoveryServiceUrl, service['restBasePath'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400153 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')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400174 setattr(method, '__is_resource__', True)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175 setattr(theclass, methodName, method)
176
177 for methodName, methodDesc in resources.iteritems():
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400178 createMethod(Service, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400179 return Service()
180
181
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400182def createResource(http, baseUrl, model, resourceName, developerKey,
183 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400184
185 class Resource(object):
186 """A class for interacting with a resource."""
187
188 def __init__(self):
189 self._http = http
190 self._baseUrl = baseUrl
191 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400192 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400193
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400194 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400195 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400196 pathUrl = re.sub(r'\{', r'{+', pathUrl)
197 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400198
ade@google.com850cf552010-08-20 23:24:56 +0100199 argmap = {}
200 if httpMethod in ['PUT', 'POST']:
201 argmap['body'] = 'body'
202
203
204 required_params = [] # Required parameters
205 pattern_params = {} # Parameters that must match a regex
206 query_params = [] # Parameters that will be used in the query string
207 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400208 if 'parameters' in methodDesc:
209 for arg, desc in methodDesc['parameters'].iteritems():
210 param = key2param(arg)
211 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400212
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400213 if desc.get('pattern', ''):
214 pattern_params[param] = desc['pattern']
215 if desc.get('required', False):
216 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400217 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400218 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400219 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400220 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400221
222 def method(self, **kwargs):
223 for name in kwargs.iterkeys():
224 if name not in argmap:
225 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400226
ade@google.com850cf552010-08-20 23:24:56 +0100227 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400228 if name not in kwargs:
229 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400230
ade@google.com850cf552010-08-20 23:24:56 +0100231 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400232 if name in kwargs:
233 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400234 raise TypeError(
235 'Parameter "%s" value "%s" does not match the pattern "%s"' %
236 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400237
ade@google.com850cf552010-08-20 23:24:56 +0100238 actual_query_params = {}
239 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400240 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100241 if key in query_params:
242 actual_query_params[argmap[key]] = value
243 if key in path_params:
244 actual_path_params[argmap[key]] = value
245 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400246
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400247 if self._developerKey:
248 actual_query_params['key'] = self._developerKey
249
Joe Gregorio48d361f2010-08-18 13:19:21 -0400250 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400251 headers, params, query, body = self._model.request(headers,
252 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400253
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100254 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
255 # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
256 # This results in an incorrect URL which returns a 404
257 url_result = urlparse.urlsplit(self._baseUrl)
258 new_base_url = url_result.scheme + '://' + url_result.netloc
259
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400260 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100261 url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400262
ade@google.com850cf552010-08-20 23:24:56 +0100263 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400264 return HttpRequest(self._http, url, method=httpMethod, body=body,
265 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400266
267 docs = ['A description of how to use this function\n\n']
268 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400269 required = ""
270 if arg in required_params:
271 required = " (required)"
272 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400273
274 setattr(method, '__doc__', ''.join(docs))
275 setattr(theclass, methodName, method)
276
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400277 def createNextMethod(theclass, methodName, methodDesc):
278
279 def method(self, previous):
280 """
281 Takes a single argument, 'body', which is the results
282 from the last call, and returns the next set of items
283 in the collection.
284
285 Returns None if there are no more items in
286 the collection.
287 """
288 if methodDesc['type'] != 'uri':
289 raise UnknownLinkType(methodDesc['type'])
290
291 try:
292 p = previous
293 for key in methodDesc['location']:
294 p = p[key]
295 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400296 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400297 return None
298
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400299 if self._developerKey:
300 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100301 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400302 q.append(('key', self._developerKey))
303 parsed[4] = urllib.urlencode(q)
304 url = urlparse.urlunparse(parsed)
305
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400306 headers = {}
307 headers, params, query, body = self._model.request(headers, {}, {}, None)
308
309 logging.info('URL being requested: %s' % url)
310 resp, content = self._http.request(url, method='GET', headers=headers)
311
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400312 return HttpRequest(self._http, url, method='GET',
313 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400314
315 setattr(theclass, methodName, method)
316
317 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400318 if 'methods' in resourceDesc:
319 for methodName, methodDesc in resourceDesc['methods'].iteritems():
320 if futureDesc:
321 future = futureDesc['methods'].get(methodName, {})
322 else:
323 future = None
324 createMethod(Resource, methodName, methodDesc, future)
325
326 # Add in nested resources
327 if 'resources' in resourceDesc:
328 def createMethod(theclass, methodName, methodDesc, futureDesc):
329
330 def method(self):
331 return createResource(self._http, self._baseUrl, self._model,
332 methodName, self._developerKey, methodDesc, futureDesc)
333
334 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400335 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400336 setattr(theclass, methodName, method)
337
338 for methodName, methodDesc in resourceDesc['resources'].iteritems():
339 if futureDesc and 'resources' in futureDesc:
340 future = futureDesc['resources'].get(methodName, {})
341 else:
342 future = {}
343 createMethod(Resource, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400344
345 # Add <m>_next() methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400346 if futureDesc:
347 for methodName, methodDesc in futureDesc['methods'].iteritems():
348 if 'next' in methodDesc and methodName in resourceDesc['methods']:
349 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400350
351 return Resource()