blob: f00188c551210634c5bd717d8a77b9b421486f25 [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 Gregorio6d5e94f2010-08-25 23:49:30 -040037class UnknownLinkType(Exception):
38 pass
39
40DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
41 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040042
43
44def key2method(key):
45 """
46 max-results -> MaxResults
47 """
48 result = []
49 key = list(key)
50 newWord = True
51 if not key[0].isalpha():
52 result.append('X')
53 newWord = False
54 for c in key:
55 if c.isalnum():
56 if newWord:
57 result.append(c.upper())
58 newWord = False
59 else:
60 result.append(c.lower())
61 else:
62 newWord = True
63
64 return ''.join(result)
65
66
67def key2param(key):
68 """
69 max-results -> max_results
70 """
71 result = []
72 key = list(key)
73 if not key[0].isalpha():
74 result.append('x')
75 for c in key:
76 if c.isalnum():
77 result.append(c)
78 else:
79 result.append('_')
80
81 return ''.join(result)
82
83
84class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040085
ade@google.com850cf552010-08-20 23:24:56 +010086 def request(self, headers, path_params, query_params, body_value):
87 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040088 headers['accept'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010089 if body_value is None:
90 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040091 else:
ade@google.com850cf552010-08-20 23:24:56 +010092 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040093 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010094 return (headers, path_params, query, simplejson.dumps(model))
95
96 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040097 params.update({'alt': 'json', 'prettyprint': 'true'})
98 astuples = []
99 for key, value in params.iteritems():
100 if getattr(value, 'encode', False) and callable(value.encode):
101 value = value.encode('utf-8')
102 astuples.append((key, value))
103 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400104
105 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400106 # Error handling is TBD, for example, do we retry
107 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400108 if resp.status < 300:
109 return simplejson.loads(content)['data']
110 else:
ade@google.com850cf552010-08-20 23:24:56 +0100111 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400112 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400113 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114 else:
115 raise HttpError(simplejson.loads(content)['error'])
116
117
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400118def build(serviceName, version, http=httplib2.Http(),
Joe Gregorio41cf7972010-08-18 15:21:06 -0400119 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400120 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400121 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400122 'apiVersion': version
123 }
ade@google.com850cf552010-08-20 23:24:56 +0100124
125 requested_url = uritemplate.expand(discoveryServiceUrl, params)
126 logging.info('URL being requested: %s' % requested_url)
127 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400128 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400129 service = d['data'][serviceName][version]
130
131 fn = os.path.join(os.path.dirname(__file__), "contrib", serviceName, "future.json")
132 f = file(fn, "r")
133 d = simplejson.load(f)
134 f.close()
135 future = d['data'][serviceName][version]['resources']
136
Joe Gregorio48d361f2010-08-18 13:19:21 -0400137 base = service['baseUrl']
138 resources = service['resources']
139
140 class Service(object):
141 """Top level interface for a service"""
142
143 def __init__(self, http=http):
144 self._http = http
145 self._baseUrl = base
146 self._model = model
147
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400148 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400149
Joe Gregorio48d361f2010-08-18 13:19:21 -0400150 def method(self, **kwargs):
151 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400152 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400153
154 setattr(method, '__doc__', 'A description of how to use this function')
155 setattr(theclass, methodName, method)
156
157 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400158 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400159 return Service()
160
161
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400162def createResource(http, baseUrl, model, resourceName, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400163
164 class Resource(object):
165 """A class for interacting with a resource."""
166
167 def __init__(self):
168 self._http = http
169 self._baseUrl = baseUrl
170 self._model = model
171
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400172 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400173 pathUrl = methodDesc['pathUrl']
174 pathUrl = re.sub(r'\{', r'{+', pathUrl)
175 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400176
ade@google.com850cf552010-08-20 23:24:56 +0100177 argmap = {}
178 if httpMethod in ['PUT', 'POST']:
179 argmap['body'] = 'body'
180
181
182 required_params = [] # Required parameters
183 pattern_params = {} # Parameters that must match a regex
184 query_params = [] # Parameters that will be used in the query string
185 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio21f11672010-08-18 17:23:17 -0400186 for arg, desc in methodDesc['parameters'].iteritems():
187 param = key2param(arg)
ade@google.com850cf552010-08-20 23:24:56 +0100188 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400189
ade@google.com850cf552010-08-20 23:24:56 +0100190 if desc.get('pattern', ''):
191 pattern_params[param] = desc['pattern']
192 if desc.get('required', False):
193 required_params.append(param)
194 if desc.get('parameterType') == 'query':
195 query_params.append(param)
196 if desc.get('parameterType') == 'path':
197 path_params[param]=param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400198
199 def method(self, **kwargs):
200 for name in kwargs.iterkeys():
201 if name not in argmap:
202 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400203
ade@google.com850cf552010-08-20 23:24:56 +0100204 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400205 if name not in kwargs:
206 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400207
ade@google.com850cf552010-08-20 23:24:56 +0100208 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400209 if name in kwargs:
210 if re.match(regex, kwargs[name]) is None:
Joe Gregorioba9ea7f2010-08-19 15:49:04 -0400211 raise TypeError('Parameter "%s" value "%s" does not match the pattern "%s"' % (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400212
ade@google.com850cf552010-08-20 23:24:56 +0100213 actual_query_params = {}
214 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400215 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100216 if key in query_params:
217 actual_query_params[argmap[key]] = value
218 if key in path_params:
219 actual_path_params[argmap[key]] = value
220 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400221
Joe Gregorio48d361f2010-08-18 13:19:21 -0400222 headers = {}
ade@google.com850cf552010-08-20 23:24:56 +0100223 headers, params, query, body = self._model.request(headers, actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400224
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400225 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100226 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400227
ade@google.com850cf552010-08-20 23:24:56 +0100228 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400229 resp, content = self._http.request(
230 url, method=httpMethod, headers=headers, body=body)
231
232 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400233
234 docs = ['A description of how to use this function\n\n']
235 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400236 required = ""
237 if arg in required_params:
238 required = " (required)"
239 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400240
241 setattr(method, '__doc__', ''.join(docs))
242 setattr(theclass, methodName, method)
243
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400244 def createNextMethod(theclass, methodName, methodDesc):
245
246 def method(self, previous):
247 """
248 Takes a single argument, 'body', which is the results
249 from the last call, and returns the next set of items
250 in the collection.
251
252 Returns None if there are no more items in
253 the collection.
254 """
255 if methodDesc['type'] != 'uri':
256 raise UnknownLinkType(methodDesc['type'])
257
258 try:
259 p = previous
260 for key in methodDesc['location']:
261 p = p[key]
262 url = p
263 except KeyError:
264 return None
265
266 headers = {}
267 headers, params, query, body = self._model.request(headers, {}, {}, None)
268
269 logging.info('URL being requested: %s' % url)
270 resp, content = self._http.request(url, method='GET', headers=headers)
271
272 return self._model.response(resp, content)
273
274 setattr(theclass, methodName, method)
275
276 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400277 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400278 createMethod(Resource, methodName, methodDesc, futureDesc['methods'].get(methodName, {}))
279
280 # Add <m>_next() methods to Resource
281 for methodName, methodDesc in futureDesc['methods'].iteritems():
282 if 'next' in methodDesc and methodName in resourceDesc['methods']:
283 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400284
285 return Resource()