blob: ea76d97562176757a0e8da5edfbb64a439f52377 [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'
ade@google.com850cf552010-08-20 23:24:56 +010067 if body_value is None:
68 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040069 else:
ade@google.com850cf552010-08-20 23:24:56 +010070 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040071 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010072 return (headers, path_params, query, simplejson.dumps(model))
73
74 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040075 params.update({'alt': 'json', 'prettyprint': 'true'})
76 astuples = []
77 for key, value in params.iteritems():
78 if getattr(value, 'encode', False) and callable(value.encode):
79 value = value.encode('utf-8')
80 astuples.append((key, value))
81 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -040082
83 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040084 # Error handling is TBD, for example, do we retry
85 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -040086 if resp.status < 300:
87 return simplejson.loads(content)['data']
88 else:
ade@google.com850cf552010-08-20 23:24:56 +010089 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -040090 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -040091 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -040092 else:
93 raise HttpError(simplejson.loads(content)['error'])
94
95
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040096def build(serviceName, version, http=httplib2.Http(),
Joe Gregorio41cf7972010-08-18 15:21:06 -040097 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -040098 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040099 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400100 'apiVersion': version
101 }
ade@google.com850cf552010-08-20 23:24:56 +0100102
103 requested_url = uritemplate.expand(discoveryServiceUrl, params)
104 logging.info('URL being requested: %s' % requested_url)
105 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400107 service = d['data'][serviceName][version]
108
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400109 fn = os.path.join(os.path.dirname(__file__), "contrib",
110 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400111 f = file(fn, "r")
112 d = simplejson.load(f)
113 f.close()
114 future = d['data'][serviceName][version]['resources']
115
Joe Gregorio48d361f2010-08-18 13:19:21 -0400116 base = service['baseUrl']
117 resources = service['resources']
118
119 class Service(object):
120 """Top level interface for a service"""
121
122 def __init__(self, http=http):
123 self._http = http
124 self._baseUrl = base
125 self._model = model
126
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400127 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400128
Joe Gregorio48d361f2010-08-18 13:19:21 -0400129 def method(self, **kwargs):
130 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400131 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400132
133 setattr(method, '__doc__', 'A description of how to use this function')
134 setattr(theclass, methodName, method)
135
136 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400137 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400138 return Service()
139
140
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400141def createResource(http, baseUrl, model, resourceName, resourceDesc,
142 futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400143
144 class Resource(object):
145 """A class for interacting with a resource."""
146
147 def __init__(self):
148 self._http = http
149 self._baseUrl = baseUrl
150 self._model = model
151
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400152 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400153 pathUrl = methodDesc['pathUrl']
154 pathUrl = re.sub(r'\{', r'{+', pathUrl)
155 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400156
ade@google.com850cf552010-08-20 23:24:56 +0100157 argmap = {}
158 if httpMethod in ['PUT', 'POST']:
159 argmap['body'] = 'body'
160
161
162 required_params = [] # Required parameters
163 pattern_params = {} # Parameters that must match a regex
164 query_params = [] # Parameters that will be used in the query string
165 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400166 if 'parameters' in methodDesc:
167 for arg, desc in methodDesc['parameters'].iteritems():
168 param = key2param(arg)
169 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400170
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400171 if desc.get('pattern', ''):
172 pattern_params[param] = desc['pattern']
173 if desc.get('required', False):
174 required_params.append(param)
175 if desc.get('parameterType') == 'query':
176 query_params.append(param)
177 if desc.get('parameterType') == 'path':
178 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400179
180 def method(self, **kwargs):
181 for name in kwargs.iterkeys():
182 if name not in argmap:
183 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400184
ade@google.com850cf552010-08-20 23:24:56 +0100185 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400186 if name not in kwargs:
187 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400188
ade@google.com850cf552010-08-20 23:24:56 +0100189 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400190 if name in kwargs:
191 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400192 raise TypeError(
193 'Parameter "%s" value "%s" does not match the pattern "%s"' %
194 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400195
ade@google.com850cf552010-08-20 23:24:56 +0100196 actual_query_params = {}
197 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400198 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100199 if key in query_params:
200 actual_query_params[argmap[key]] = value
201 if key in path_params:
202 actual_path_params[argmap[key]] = value
203 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400204
Joe Gregorio48d361f2010-08-18 13:19:21 -0400205 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400206 headers, params, query, body = self._model.request(headers,
207 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400208
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400209 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100210 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400211
ade@google.com850cf552010-08-20 23:24:56 +0100212 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400213 resp, content = self._http.request(
214 url, method=httpMethod, headers=headers, body=body)
215
216 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400217
218 docs = ['A description of how to use this function\n\n']
219 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400220 required = ""
221 if arg in required_params:
222 required = " (required)"
223 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400224
225 setattr(method, '__doc__', ''.join(docs))
226 setattr(theclass, methodName, method)
227
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400228 def createNextMethod(theclass, methodName, methodDesc):
229
230 def method(self, previous):
231 """
232 Takes a single argument, 'body', which is the results
233 from the last call, and returns the next set of items
234 in the collection.
235
236 Returns None if there are no more items in
237 the collection.
238 """
239 if methodDesc['type'] != 'uri':
240 raise UnknownLinkType(methodDesc['type'])
241
242 try:
243 p = previous
244 for key in methodDesc['location']:
245 p = p[key]
246 url = p
247 except KeyError:
248 return None
249
250 headers = {}
251 headers, params, query, body = self._model.request(headers, {}, {}, None)
252
253 logging.info('URL being requested: %s' % url)
254 resp, content = self._http.request(url, method='GET', headers=headers)
255
256 return self._model.response(resp, content)
257
258 setattr(theclass, methodName, method)
259
260 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400261 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400262 future = futureDesc['methods'].get(methodName, {})
263 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400264
265 # Add <m>_next() methods to Resource
266 for methodName, methodDesc in futureDesc['methods'].iteritems():
267 if 'next' in methodDesc and methodName in resourceDesc['methods']:
268 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400269
270 return Resource()