blob: 5a0ba33851e5b0adead461a557ca4e70bf3fe800 [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 Gregorio48d361f2010-08-18 13:19:21 -040031
Joe Gregorioe6efd532010-09-20 11:13:50 -040032try:
33 import simplejson
34except ImportError:
35 try:
36 # Try to import from django, should work on App Engine
37 from django.utils import simplejson
38 except ImportError:
39 # Should work for Python2.6 and higher.
40 import json as simplejson
41
Joe Gregorio48d361f2010-08-18 13:19:21 -040042
Joe Gregorio41cf7972010-08-18 15:21:06 -040043class HttpError(Exception):
44 pass
45
Joe Gregorio3bbbf662010-08-30 16:41:53 -040046
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040047class UnknownLinkType(Exception):
48 pass
49
50DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
51 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040052
53
Joe Gregorio48d361f2010-08-18 13:19:21 -040054def key2param(key):
55 """
56 max-results -> max_results
57 """
58 result = []
59 key = list(key)
60 if not key[0].isalpha():
61 result.append('x')
62 for c in key:
63 if c.isalnum():
64 result.append(c)
65 else:
66 result.append('_')
67
68 return ''.join(result)
69
70
71class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040072
ade@google.com850cf552010-08-20 23:24:56 +010073 def request(self, headers, path_params, query_params, body_value):
74 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040075 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040076 if 'user-agent' in headers:
77 headers['user-agent'] += ' '
78 else:
79 headers['user-agent'] = ''
Joe Gregorioe9369582010-09-14 15:17:28 -040080 headers['user-agent'] += 'google-api-python-client/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010081 if body_value is None:
82 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040083 else:
ade@google.com850cf552010-08-20 23:24:56 +010084 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040085 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010086 return (headers, path_params, query, simplejson.dumps(model))
87
88 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040089 params.update({'alt': 'json', 'prettyprint': 'true'})
90 astuples = []
91 for key, value in params.iteritems():
92 if getattr(value, 'encode', False) and callable(value.encode):
93 value = value.encode('utf-8')
94 astuples.append((key, value))
95 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -040096
97 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040098 # Error handling is TBD, for example, do we retry
99 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400100 if resp.status < 300:
101 return simplejson.loads(content)['data']
102 else:
ade@google.com850cf552010-08-20 23:24:56 +0100103 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400104 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400105 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106 else:
107 raise HttpError(simplejson.loads(content)['error'])
108
109
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400110def build(serviceName, version, http=httplib2.Http(),
Joe Gregorio41cf7972010-08-18 15:21:06 -0400111 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400112 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400113 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114 'apiVersion': version
115 }
ade@google.com850cf552010-08-20 23:24:56 +0100116
117 requested_url = uritemplate.expand(discoveryServiceUrl, params)
118 logging.info('URL being requested: %s' % requested_url)
119 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400120 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400121 service = d['data'][serviceName][version]
122
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400123 fn = os.path.join(os.path.dirname(__file__), "contrib",
124 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400125 f = file(fn, "r")
126 d = simplejson.load(f)
127 f.close()
128 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400129 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400130
Joe Gregorio48d361f2010-08-18 13:19:21 -0400131 base = service['baseUrl']
132 resources = service['resources']
133
134 class Service(object):
135 """Top level interface for a service"""
136
137 def __init__(self, http=http):
138 self._http = http
139 self._baseUrl = base
140 self._model = model
141
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400142 def auth_discovery(self):
143 return auth_discovery
144
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400145 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400146
Joe Gregorio48d361f2010-08-18 13:19:21 -0400147 def method(self, **kwargs):
148 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400149 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400150
151 setattr(method, '__doc__', 'A description of how to use this function')
152 setattr(theclass, methodName, method)
153
154 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400155 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400156 return Service()
157
158
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400159def createResource(http, baseUrl, model, resourceName, resourceDesc,
160 futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400161
162 class Resource(object):
163 """A class for interacting with a resource."""
164
165 def __init__(self):
166 self._http = http
167 self._baseUrl = baseUrl
168 self._model = model
169
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400170 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400171 pathUrl = methodDesc['pathUrl']
172 pathUrl = re.sub(r'\{', r'{+', pathUrl)
173 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400174
ade@google.com850cf552010-08-20 23:24:56 +0100175 argmap = {}
176 if httpMethod in ['PUT', 'POST']:
177 argmap['body'] = 'body'
178
179
180 required_params = [] # Required parameters
181 pattern_params = {} # Parameters that must match a regex
182 query_params = [] # Parameters that will be used in the query string
183 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400184 if 'parameters' in methodDesc:
185 for arg, desc in methodDesc['parameters'].iteritems():
186 param = key2param(arg)
187 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400188
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400189 if desc.get('pattern', ''):
190 pattern_params[param] = desc['pattern']
191 if desc.get('required', False):
192 required_params.append(param)
193 if desc.get('parameterType') == 'query':
194 query_params.append(param)
195 if desc.get('parameterType') == 'path':
196 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400197
198 def method(self, **kwargs):
199 for name in kwargs.iterkeys():
200 if name not in argmap:
201 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400202
ade@google.com850cf552010-08-20 23:24:56 +0100203 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400204 if name not in kwargs:
205 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400206
ade@google.com850cf552010-08-20 23:24:56 +0100207 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400208 if name in kwargs:
209 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400210 raise TypeError(
211 'Parameter "%s" value "%s" does not match the pattern "%s"' %
212 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400213
ade@google.com850cf552010-08-20 23:24:56 +0100214 actual_query_params = {}
215 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400216 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100217 if key in query_params:
218 actual_query_params[argmap[key]] = value
219 if key in path_params:
220 actual_path_params[argmap[key]] = value
221 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400222
Joe Gregorio48d361f2010-08-18 13:19:21 -0400223 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400224 headers, params, query, body = self._model.request(headers,
225 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400226
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400227 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100228 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400229
ade@google.com850cf552010-08-20 23:24:56 +0100230 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400231 resp, content = self._http.request(
232 url, method=httpMethod, headers=headers, body=body)
233
234 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400235
236 docs = ['A description of how to use this function\n\n']
237 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400238 required = ""
239 if arg in required_params:
240 required = " (required)"
241 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400242
243 setattr(method, '__doc__', ''.join(docs))
244 setattr(theclass, methodName, method)
245
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400246 def createNextMethod(theclass, methodName, methodDesc):
247
248 def method(self, previous):
249 """
250 Takes a single argument, 'body', which is the results
251 from the last call, and returns the next set of items
252 in the collection.
253
254 Returns None if there are no more items in
255 the collection.
256 """
257 if methodDesc['type'] != 'uri':
258 raise UnknownLinkType(methodDesc['type'])
259
260 try:
261 p = previous
262 for key in methodDesc['location']:
263 p = p[key]
264 url = p
265 except KeyError:
266 return None
267
268 headers = {}
269 headers, params, query, body = self._model.request(headers, {}, {}, None)
270
271 logging.info('URL being requested: %s' % url)
272 resp, content = self._http.request(url, method='GET', headers=headers)
273
274 return self._model.response(resp, content)
275
276 setattr(theclass, methodName, method)
277
278 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400279 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400280 future = futureDesc['methods'].get(methodName, {})
281 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400282
283 # Add <m>_next() methods to Resource
284 for methodName, methodDesc in futureDesc['methods'].iteritems():
285 if 'next' in methodDesc and methodName in resourceDesc['methods']:
286 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400287
288 return Resource()