blob: 1bac08cbdb9978685439eabfd95b50b44ccc410e [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
28import simplejson
Joe Gregorio48d361f2010-08-18 13:19:21 -040029import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040030import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040031import urlparse
Joe Gregorio48d361f2010-08-18 13:19:21 -040032
Joe Gregorio48d361f2010-08-18 13:19:21 -040033
Joe Gregorio41cf7972010-08-18 15:21:06 -040034class HttpError(Exception):
35 pass
36
Joe Gregorio3bbbf662010-08-30 16:41:53 -040037
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040038class UnknownLinkType(Exception):
39 pass
40
41DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
42 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040043
44
Joe Gregorio48d361f2010-08-18 13:19:21 -040045def key2param(key):
46 """
47 max-results -> max_results
48 """
49 result = []
50 key = list(key)
51 if not key[0].isalpha():
52 result.append('x')
53 for c in key:
54 if c.isalnum():
55 result.append(c)
56 else:
57 result.append('_')
58
59 return ''.join(result)
60
61
62class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040063
ade@google.com850cf552010-08-20 23:24:56 +010064 def request(self, headers, path_params, query_params, body_value):
65 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040066 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040067 if 'user-agent' in headers:
68 headers['user-agent'] += ' '
69 else:
70 headers['user-agent'] = ''
71 headers['user-agent'] += 'google-api-client-python/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010072 if body_value is None:
73 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040074 else:
ade@google.com850cf552010-08-20 23:24:56 +010075 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040076 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010077 return (headers, path_params, query, simplejson.dumps(model))
78
79 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040080 params.update({'alt': 'json', 'prettyprint': 'true'})
81 astuples = []
82 for key, value in params.iteritems():
83 if getattr(value, 'encode', False) and callable(value.encode):
84 value = value.encode('utf-8')
85 astuples.append((key, value))
86 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -040087
88 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040089 # Error handling is TBD, for example, do we retry
90 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -040091 if resp.status < 300:
92 return simplejson.loads(content)['data']
93 else:
ade@google.com850cf552010-08-20 23:24:56 +010094 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -040095 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -040096 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -040097 else:
98 raise HttpError(simplejson.loads(content)['error'])
99
100
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400101def build(serviceName, version, http=httplib2.Http(),
Joe Gregorio41cf7972010-08-18 15:21:06 -0400102 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400103 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400104 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400105 'apiVersion': version
106 }
ade@google.com850cf552010-08-20 23:24:56 +0100107
108 requested_url = uritemplate.expand(discoveryServiceUrl, params)
109 logging.info('URL being requested: %s' % requested_url)
110 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400111 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400112 service = d['data'][serviceName][version]
113
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400114 fn = os.path.join(os.path.dirname(__file__), "contrib",
115 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400116 f = file(fn, "r")
117 d = simplejson.load(f)
118 f.close()
119 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400120 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400121
Joe Gregorio48d361f2010-08-18 13:19:21 -0400122 base = service['baseUrl']
123 resources = service['resources']
124
125 class Service(object):
126 """Top level interface for a service"""
127
128 def __init__(self, http=http):
129 self._http = http
130 self._baseUrl = base
131 self._model = model
132
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400133 def auth_discovery(self):
134 return auth_discovery
135
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400136 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400137
Joe Gregorio48d361f2010-08-18 13:19:21 -0400138 def method(self, **kwargs):
139 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400140 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400141
142 setattr(method, '__doc__', 'A description of how to use this function')
143 setattr(theclass, methodName, method)
144
145 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400146 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400147 return Service()
148
149
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400150def createResource(http, baseUrl, model, resourceName, resourceDesc,
151 futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400152
153 class Resource(object):
154 """A class for interacting with a resource."""
155
156 def __init__(self):
157 self._http = http
158 self._baseUrl = baseUrl
159 self._model = model
160
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400161 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400162 pathUrl = methodDesc['pathUrl']
163 pathUrl = re.sub(r'\{', r'{+', pathUrl)
164 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400165
ade@google.com850cf552010-08-20 23:24:56 +0100166 argmap = {}
167 if httpMethod in ['PUT', 'POST']:
168 argmap['body'] = 'body'
169
170
171 required_params = [] # Required parameters
172 pattern_params = {} # Parameters that must match a regex
173 query_params = [] # Parameters that will be used in the query string
174 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400175 if 'parameters' in methodDesc:
176 for arg, desc in methodDesc['parameters'].iteritems():
177 param = key2param(arg)
178 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400179
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400180 if desc.get('pattern', ''):
181 pattern_params[param] = desc['pattern']
182 if desc.get('required', False):
183 required_params.append(param)
184 if desc.get('parameterType') == 'query':
185 query_params.append(param)
186 if desc.get('parameterType') == 'path':
187 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400188
189 def method(self, **kwargs):
190 for name in kwargs.iterkeys():
191 if name not in argmap:
192 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400193
ade@google.com850cf552010-08-20 23:24:56 +0100194 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400195 if name not in kwargs:
196 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400197
ade@google.com850cf552010-08-20 23:24:56 +0100198 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400199 if name in kwargs:
200 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400201 raise TypeError(
202 'Parameter "%s" value "%s" does not match the pattern "%s"' %
203 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400204
ade@google.com850cf552010-08-20 23:24:56 +0100205 actual_query_params = {}
206 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400207 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100208 if key in query_params:
209 actual_query_params[argmap[key]] = value
210 if key in path_params:
211 actual_path_params[argmap[key]] = value
212 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400213
Joe Gregorio48d361f2010-08-18 13:19:21 -0400214 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400215 headers, params, query, body = self._model.request(headers,
216 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400217
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400218 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100219 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400220
ade@google.com850cf552010-08-20 23:24:56 +0100221 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400222 resp, content = self._http.request(
223 url, method=httpMethod, headers=headers, body=body)
224
225 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400226
227 docs = ['A description of how to use this function\n\n']
228 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400229 required = ""
230 if arg in required_params:
231 required = " (required)"
232 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400233
234 setattr(method, '__doc__', ''.join(docs))
235 setattr(theclass, methodName, method)
236
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400237 def createNextMethod(theclass, methodName, methodDesc):
238
239 def method(self, previous):
240 """
241 Takes a single argument, 'body', which is the results
242 from the last call, and returns the next set of items
243 in the collection.
244
245 Returns None if there are no more items in
246 the collection.
247 """
248 if methodDesc['type'] != 'uri':
249 raise UnknownLinkType(methodDesc['type'])
250
251 try:
252 p = previous
253 for key in methodDesc['location']:
254 p = p[key]
255 url = p
256 except KeyError:
257 return None
258
259 headers = {}
260 headers, params, query, body = self._model.request(headers, {}, {}, None)
261
262 logging.info('URL being requested: %s' % url)
263 resp, content = self._http.request(url, method='GET', headers=headers)
264
265 return self._model.response(resp, content)
266
267 setattr(theclass, methodName, method)
268
269 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400270 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400271 future = futureDesc['methods'].get(methodName, {})
272 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400273
274 # Add <m>_next() methods to Resource
275 for methodName, methodDesc in futureDesc['methods'].iteritems():
276 if 'next' in methodDesc and methodName in resourceDesc['methods']:
277 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400278
279 return Resource()