blob: 328d9705b811da35bc40c77fbbde7ad37708d6b0 [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
Joe Gregorio41cf7972010-08-18 15:21:06 -040048class HttpError(Exception):
49 pass
50
Joe Gregorio3bbbf662010-08-30 16:41:53 -040051
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040052class UnknownLinkType(Exception):
53 pass
54
55DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
56 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040057
58
Joe Gregorio48d361f2010-08-18 13:19:21 -040059def key2param(key):
60 """
61 max-results -> max_results
62 """
63 result = []
64 key = list(key)
65 if not key[0].isalpha():
66 result.append('x')
67 for c in key:
68 if c.isalnum():
69 result.append(c)
70 else:
71 result.append('_')
72
73 return ''.join(result)
74
75
76class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040077
ade@google.com850cf552010-08-20 23:24:56 +010078 def request(self, headers, path_params, query_params, body_value):
79 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040080 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040081 if 'user-agent' in headers:
82 headers['user-agent'] += ' '
83 else:
84 headers['user-agent'] = ''
Joe Gregorioe9369582010-09-14 15:17:28 -040085 headers['user-agent'] += 'google-api-python-client/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010086 if body_value is None:
87 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040088 else:
ade@google.com850cf552010-08-20 23:24:56 +010089 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040090 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010091 return (headers, path_params, query, simplejson.dumps(model))
92
93 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040094 params.update({'alt': 'json', 'prettyprint': 'true'})
95 astuples = []
96 for key, value in params.iteritems():
97 if getattr(value, 'encode', False) and callable(value.encode):
98 value = value.encode('utf-8')
99 astuples.append((key, value))
100 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400101
102 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400103 # Error handling is TBD, for example, do we retry
104 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400105 if resp.status < 300:
106 return simplejson.loads(content)['data']
107 else:
ade@google.com850cf552010-08-20 23:24:56 +0100108 logging.debug('Content from bad request was: %s' % content)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400109 if resp.get('content-type', '') != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400110 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400111 else:
112 raise HttpError(simplejson.loads(content)['error'])
113
114
Joe Gregorioc204b642010-09-21 12:01:23 -0400115def build(serviceName, version, http=None,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400116 discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400117 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400118 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400119 'apiVersion': version
120 }
ade@google.com850cf552010-08-20 23:24:56 +0100121
Joe Gregorioc204b642010-09-21 12:01:23 -0400122 if http is None:
123 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100124 requested_url = uritemplate.expand(discoveryServiceUrl, params)
125 logging.info('URL being requested: %s' % requested_url)
126 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400127 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400128 service = d['data'][serviceName][version]
129
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400130 fn = os.path.join(os.path.dirname(__file__), "contrib",
131 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400132 f = file(fn, "r")
133 d = simplejson.load(f)
134 f.close()
135 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400136 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400137
Joe Gregorio48d361f2010-08-18 13:19:21 -0400138 base = service['baseUrl']
139 resources = service['resources']
140
141 class Service(object):
142 """Top level interface for a service"""
143
144 def __init__(self, http=http):
145 self._http = http
146 self._baseUrl = base
147 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400148 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400149
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400150 def auth_discovery(self):
151 return auth_discovery
152
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400153 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400154
Joe Gregorio59cd9512010-10-04 12:46:46 -0400155 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400156 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400157 methodName, self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400158
159 setattr(method, '__doc__', 'A description of how to use this function')
160 setattr(theclass, methodName, method)
161
162 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400163 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400164 return Service()
165
166
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400167def createResource(http, baseUrl, model, resourceName, developerKey,
168 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400169
170 class Resource(object):
171 """A class for interacting with a resource."""
172
173 def __init__(self):
174 self._http = http
175 self._baseUrl = baseUrl
176 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400177 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400178
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400179 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400180 pathUrl = methodDesc['pathUrl']
181 pathUrl = re.sub(r'\{', r'{+', pathUrl)
182 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400183
ade@google.com850cf552010-08-20 23:24:56 +0100184 argmap = {}
185 if httpMethod in ['PUT', 'POST']:
186 argmap['body'] = 'body'
187
188
189 required_params = [] # Required parameters
190 pattern_params = {} # Parameters that must match a regex
191 query_params = [] # Parameters that will be used in the query string
192 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400193 if 'parameters' in methodDesc:
194 for arg, desc in methodDesc['parameters'].iteritems():
195 param = key2param(arg)
196 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400197
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400198 if desc.get('pattern', ''):
199 pattern_params[param] = desc['pattern']
200 if desc.get('required', False):
201 required_params.append(param)
202 if desc.get('parameterType') == 'query':
203 query_params.append(param)
204 if desc.get('parameterType') == 'path':
205 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400206
207 def method(self, **kwargs):
208 for name in kwargs.iterkeys():
209 if name not in argmap:
210 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400211
ade@google.com850cf552010-08-20 23:24:56 +0100212 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400213 if name not in kwargs:
214 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400215
ade@google.com850cf552010-08-20 23:24:56 +0100216 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400217 if name in kwargs:
218 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400219 raise TypeError(
220 'Parameter "%s" value "%s" does not match the pattern "%s"' %
221 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400222
ade@google.com850cf552010-08-20 23:24:56 +0100223 actual_query_params = {}
224 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400225 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100226 if key in query_params:
227 actual_query_params[argmap[key]] = value
228 if key in path_params:
229 actual_path_params[argmap[key]] = value
230 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400231
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400232 if self._developerKey:
233 actual_query_params['key'] = self._developerKey
234
Joe Gregorio48d361f2010-08-18 13:19:21 -0400235 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400236 headers, params, query, body = self._model.request(headers,
237 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400238
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100239 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
240 # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
241 # This results in an incorrect URL which returns a 404
242 url_result = urlparse.urlsplit(self._baseUrl)
243 new_base_url = url_result.scheme + '://' + url_result.netloc
244
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400245 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100246 url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400247
ade@google.com850cf552010-08-20 23:24:56 +0100248 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400249 return HttpRequest(self._http, url, method=httpMethod, body=body,
250 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400251
252 docs = ['A description of how to use this function\n\n']
253 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400254 required = ""
255 if arg in required_params:
256 required = " (required)"
257 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400258
259 setattr(method, '__doc__', ''.join(docs))
260 setattr(theclass, methodName, method)
261
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400262 def createNextMethod(theclass, methodName, methodDesc):
263
264 def method(self, previous):
265 """
266 Takes a single argument, 'body', which is the results
267 from the last call, and returns the next set of items
268 in the collection.
269
270 Returns None if there are no more items in
271 the collection.
272 """
273 if methodDesc['type'] != 'uri':
274 raise UnknownLinkType(methodDesc['type'])
275
276 try:
277 p = previous
278 for key in methodDesc['location']:
279 p = p[key]
280 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400281 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400282 return None
283
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400284 if self._developerKey:
285 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100286 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400287 q.append(('key', self._developerKey))
288 parsed[4] = urllib.urlencode(q)
289 url = urlparse.urlunparse(parsed)
290
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400291 headers = {}
292 headers, params, query, body = self._model.request(headers, {}, {}, None)
293
294 logging.info('URL being requested: %s' % url)
295 resp, content = self._http.request(url, method='GET', headers=headers)
296
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400297 return HttpRequest(self._http, url, method='GET',
298 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400299
300 setattr(theclass, methodName, method)
301
302 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400303 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400304 future = futureDesc['methods'].get(methodName, {})
305 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400306
307 # Add <m>_next() methods to Resource
308 for methodName, methodDesc in futureDesc['methods'].iteritems():
309 if 'next' in methodDesc and methodName in resourceDesc['methods']:
310 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400311
312 return Resource()