blob: bb5c305046effa0e1e378d2666377123be2d3f54 [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
Joe Gregorio5f087cf2010-09-20 16:08:07 -040031from apiclient.http import HttpRequest
Joe Gregorio48d361f2010-08-18 13:19:21 -040032
Joe Gregoriodb849af2010-09-22 16:53:59 -040033try: # pragma: no cover
Joe Gregorioe6efd532010-09-20 11:13:50 -040034 import simplejson
Joe Gregoriodb849af2010-09-22 16:53:59 -040035except ImportError: # pragma: no cover
Joe Gregorioe6efd532010-09-20 11:13:50 -040036 try:
37 # Try to import from django, should work on App Engine
38 from django.utils import simplejson
39 except ImportError:
40 # Should work for Python2.6 and higher.
41 import json as simplejson
42
Joe Gregorio48d361f2010-08-18 13:19:21 -040043
Joe Gregorio41cf7972010-08-18 15:21:06 -040044class HttpError(Exception):
45 pass
46
Joe Gregorio3bbbf662010-08-30 16:41:53 -040047
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040048class UnknownLinkType(Exception):
49 pass
50
51DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
52 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040053
54
Joe Gregorio48d361f2010-08-18 13:19:21 -040055def key2param(key):
56 """
57 max-results -> max_results
58 """
59 result = []
60 key = list(key)
61 if not key[0].isalpha():
62 result.append('x')
63 for c in key:
64 if c.isalnum():
65 result.append(c)
66 else:
67 result.append('_')
68
69 return ''.join(result)
70
71
72class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040073
ade@google.com850cf552010-08-20 23:24:56 +010074 def request(self, headers, path_params, query_params, body_value):
75 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040076 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040077 if 'user-agent' in headers:
78 headers['user-agent'] += ' '
79 else:
80 headers['user-agent'] = ''
Joe Gregorioe9369582010-09-14 15:17:28 -040081 headers['user-agent'] += 'google-api-python-client/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010082 if body_value is None:
83 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040084 else:
ade@google.com850cf552010-08-20 23:24:56 +010085 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040086 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010087 return (headers, path_params, query, simplejson.dumps(model))
88
89 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040090 params.update({'alt': 'json', 'prettyprint': 'true'})
91 astuples = []
92 for key, value in params.iteritems():
93 if getattr(value, 'encode', False) and callable(value.encode):
94 value = value.encode('utf-8')
95 astuples.append((key, value))
96 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -040097
98 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040099 # Error handling is TBD, for example, do we retry
100 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400101 if resp.status < 300:
102 return simplejson.loads(content)['data']
103 else:
ade@google.com850cf552010-08-20 23:24:56 +0100104 logging.debug('Content from bad request was: %s' % content)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400105 if resp.get('content-type', '') != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400106 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400107 else:
108 raise HttpError(simplejson.loads(content)['error'])
109
110
Joe Gregorioc204b642010-09-21 12:01:23 -0400111def build(serviceName, version, http=None,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400112 discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400113 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400114 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400115 'apiVersion': version
116 }
ade@google.com850cf552010-08-20 23:24:56 +0100117
Joe Gregorioc204b642010-09-21 12:01:23 -0400118 if http is None:
119 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100120 requested_url = uritemplate.expand(discoveryServiceUrl, params)
121 logging.info('URL being requested: %s' % requested_url)
122 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400123 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400124 service = d['data'][serviceName][version]
125
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400126 fn = os.path.join(os.path.dirname(__file__), "contrib",
127 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400128 f = file(fn, "r")
129 d = simplejson.load(f)
130 f.close()
131 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400132 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400133
Joe Gregorio48d361f2010-08-18 13:19:21 -0400134 base = service['baseUrl']
135 resources = service['resources']
136
137 class Service(object):
138 """Top level interface for a service"""
139
140 def __init__(self, http=http):
141 self._http = http
142 self._baseUrl = base
143 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400144 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400145
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400146 def auth_discovery(self):
147 return auth_discovery
148
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400149 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400150
Joe Gregorio48d361f2010-08-18 13:19:21 -0400151 def method(self, **kwargs):
152 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400153 methodName, self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400154
155 setattr(method, '__doc__', 'A description of how to use this function')
156 setattr(theclass, methodName, method)
157
158 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400159 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400160 return Service()
161
162
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400163def createResource(http, baseUrl, model, resourceName, developerKey,
164 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400165
166 class Resource(object):
167 """A class for interacting with a resource."""
168
169 def __init__(self):
170 self._http = http
171 self._baseUrl = baseUrl
172 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400173 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400174
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400175 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400176 pathUrl = methodDesc['pathUrl']
177 pathUrl = re.sub(r'\{', r'{+', pathUrl)
178 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400179
ade@google.com850cf552010-08-20 23:24:56 +0100180 argmap = {}
181 if httpMethod in ['PUT', 'POST']:
182 argmap['body'] = 'body'
183
184
185 required_params = [] # Required parameters
186 pattern_params = {} # Parameters that must match a regex
187 query_params = [] # Parameters that will be used in the query string
188 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400189 if 'parameters' in methodDesc:
190 for arg, desc in methodDesc['parameters'].iteritems():
191 param = key2param(arg)
192 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400193
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400194 if desc.get('pattern', ''):
195 pattern_params[param] = desc['pattern']
196 if desc.get('required', False):
197 required_params.append(param)
198 if desc.get('parameterType') == 'query':
199 query_params.append(param)
200 if desc.get('parameterType') == 'path':
201 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400202
203 def method(self, **kwargs):
204 for name in kwargs.iterkeys():
205 if name not in argmap:
206 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400207
ade@google.com850cf552010-08-20 23:24:56 +0100208 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400209 if name not in kwargs:
210 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400211
ade@google.com850cf552010-08-20 23:24:56 +0100212 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400213 if name in kwargs:
214 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400215 raise TypeError(
216 'Parameter "%s" value "%s" does not match the pattern "%s"' %
217 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400218
ade@google.com850cf552010-08-20 23:24:56 +0100219 actual_query_params = {}
220 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400221 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100222 if key in query_params:
223 actual_query_params[argmap[key]] = value
224 if key in path_params:
225 actual_path_params[argmap[key]] = value
226 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400227
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400228 if self._developerKey:
229 actual_query_params['key'] = self._developerKey
230
Joe Gregorio48d361f2010-08-18 13:19:21 -0400231 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400232 headers, params, query, body = self._model.request(headers,
233 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400234
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400235 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100236 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400237
ade@google.com850cf552010-08-20 23:24:56 +0100238 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400239 return HttpRequest(self._http, url, method=httpMethod, body=body,
240 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400241
242 docs = ['A description of how to use this function\n\n']
243 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400244 required = ""
245 if arg in required_params:
246 required = " (required)"
247 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400248
249 setattr(method, '__doc__', ''.join(docs))
250 setattr(theclass, methodName, method)
251
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400252 def createNextMethod(theclass, methodName, methodDesc):
253
254 def method(self, previous):
255 """
256 Takes a single argument, 'body', which is the results
257 from the last call, and returns the next set of items
258 in the collection.
259
260 Returns None if there are no more items in
261 the collection.
262 """
263 if methodDesc['type'] != 'uri':
264 raise UnknownLinkType(methodDesc['type'])
265
266 try:
267 p = previous
268 for key in methodDesc['location']:
269 p = p[key]
270 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400271 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400272 return None
273
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400274 if self._developerKey:
275 parsed = list(urlparse.urlparse(url))
276 q = urlparse.parse_qsl(parsed[4])
277 q.append(('key', self._developerKey))
278 parsed[4] = urllib.urlencode(q)
279 url = urlparse.urlunparse(parsed)
280
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400281 headers = {}
282 headers, params, query, body = self._model.request(headers, {}, {}, None)
283
284 logging.info('URL being requested: %s' % url)
285 resp, content = self._http.request(url, method='GET', headers=headers)
286
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400287 return HttpRequest(self._http, url, method='GET',
288 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400289
290 setattr(theclass, methodName, method)
291
292 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400293 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400294 future = futureDesc['methods'].get(methodName, {})
295 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400296
297 # Add <m>_next() methods to Resource
298 for methodName, methodDesc in futureDesc['methods'].iteritems():
299 if 'next' in methodDesc and methodName in resourceDesc['methods']:
300 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400301
302 return Resource()