blob: d9fd085720f3545622977bf9ada032cc4a6ef4fc [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 Gregorio6d5e94f2010-08-25 23:49:30 -040030import urlparse
Joe Gregorio48d361f2010-08-18 13:19:21 -040031
Joe Gregorio48d361f2010-08-18 13:19:21 -040032
Joe Gregorio41cf7972010-08-18 15:21:06 -040033class HttpError(Exception):
34 pass
35
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040036class UnknownLinkType(Exception):
37 pass
38
39DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
40 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040041
42
43def key2method(key):
44 """
45 max-results -> MaxResults
46 """
47 result = []
48 key = list(key)
49 newWord = True
50 if not key[0].isalpha():
51 result.append('X')
52 newWord = False
53 for c in key:
54 if c.isalnum():
55 if newWord:
56 result.append(c.upper())
57 newWord = False
58 else:
59 result.append(c.lower())
60 else:
61 newWord = True
62
63 return ''.join(result)
64
65
66def key2param(key):
67 """
68 max-results -> max_results
69 """
70 result = []
71 key = list(key)
72 if not key[0].isalpha():
73 result.append('x')
74 for c in key:
75 if c.isalnum():
76 result.append(c)
77 else:
78 result.append('_')
79
80 return ''.join(result)
81
82
83class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040084
ade@google.com850cf552010-08-20 23:24:56 +010085 def request(self, headers, path_params, query_params, body_value):
86 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040087 headers['accept'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010088 if body_value is None:
89 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040090 else:
ade@google.com850cf552010-08-20 23:24:56 +010091 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040092 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010093 return (headers, path_params, query, simplejson.dumps(model))
94
95 def build_query(self, params):
96 query = '?alt=json&prettyprint=true'
97 for key,value in params.iteritems():
98 query += '&%s=%s' % (key, value)
99 return query
Joe Gregorio48d361f2010-08-18 13:19:21 -0400100
101 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400102 # Error handling is TBD, for example, do we retry
103 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400104 if resp.status < 300:
105 return simplejson.loads(content)['data']
106 else:
ade@google.com850cf552010-08-20 23:24:56 +0100107 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400108 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400109 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400110 else:
111 raise HttpError(simplejson.loads(content)['error'])
112
113
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400114def build(serviceName, version, http=httplib2.Http(),
Joe Gregorio41cf7972010-08-18 15:21:06 -0400115 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400116 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400117 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400118 'apiVersion': version
119 }
ade@google.com850cf552010-08-20 23:24:56 +0100120
121 requested_url = uritemplate.expand(discoveryServiceUrl, params)
122 logging.info('URL being requested: %s' % requested_url)
123 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400124 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400125 service = d['data'][serviceName][version]
126
127 fn = os.path.join(os.path.dirname(__file__), "contrib", serviceName, "future.json")
128 f = file(fn, "r")
129 d = simplejson.load(f)
130 f.close()
131 future = d['data'][serviceName][version]['resources']
132
Joe Gregorio48d361f2010-08-18 13:19:21 -0400133 base = service['baseUrl']
134 resources = service['resources']
135
136 class Service(object):
137 """Top level interface for a service"""
138
139 def __init__(self, http=http):
140 self._http = http
141 self._baseUrl = base
142 self._model = model
143
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400144 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400145
Joe Gregorio48d361f2010-08-18 13:19:21 -0400146 def method(self, **kwargs):
147 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400148 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400149
150 setattr(method, '__doc__', 'A description of how to use this function')
151 setattr(theclass, methodName, method)
152
153 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400154 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400155 return Service()
156
157
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400158def createResource(http, baseUrl, model, resourceName, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400159
160 class Resource(object):
161 """A class for interacting with a resource."""
162
163 def __init__(self):
164 self._http = http
165 self._baseUrl = baseUrl
166 self._model = model
167
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400168 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400169 pathUrl = methodDesc['pathUrl']
170 pathUrl = re.sub(r'\{', r'{+', pathUrl)
171 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400172
ade@google.com850cf552010-08-20 23:24:56 +0100173 argmap = {}
174 if httpMethod in ['PUT', 'POST']:
175 argmap['body'] = 'body'
176
177
178 required_params = [] # Required parameters
179 pattern_params = {} # Parameters that must match a regex
180 query_params = [] # Parameters that will be used in the query string
181 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio21f11672010-08-18 17:23:17 -0400182 for arg, desc in methodDesc['parameters'].iteritems():
183 param = key2param(arg)
ade@google.com850cf552010-08-20 23:24:56 +0100184 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400185
ade@google.com850cf552010-08-20 23:24:56 +0100186 if desc.get('pattern', ''):
187 pattern_params[param] = desc['pattern']
188 if desc.get('required', False):
189 required_params.append(param)
190 if desc.get('parameterType') == 'query':
191 query_params.append(param)
192 if desc.get('parameterType') == 'path':
193 path_params[param]=param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400194
195 def method(self, **kwargs):
196 for name in kwargs.iterkeys():
197 if name not in argmap:
198 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400199
ade@google.com850cf552010-08-20 23:24:56 +0100200 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400201 if name not in kwargs:
202 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400203
ade@google.com850cf552010-08-20 23:24:56 +0100204 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400205 if name in kwargs:
206 if re.match(regex, kwargs[name]) is None:
Joe Gregorioba9ea7f2010-08-19 15:49:04 -0400207 raise TypeError('Parameter "%s" value "%s" does not match the pattern "%s"' % (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400208
ade@google.com850cf552010-08-20 23:24:56 +0100209 actual_query_params = {}
210 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400211 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100212 if key in query_params:
213 actual_query_params[argmap[key]] = value
214 if key in path_params:
215 actual_path_params[argmap[key]] = value
216 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400217
Joe Gregorio48d361f2010-08-18 13:19:21 -0400218 headers = {}
ade@google.com850cf552010-08-20 23:24:56 +0100219 headers, params, query, body = self._model.request(headers, actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400220
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400221 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100222 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400223
ade@google.com850cf552010-08-20 23:24:56 +0100224 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400225 resp, content = self._http.request(
226 url, method=httpMethod, headers=headers, body=body)
227
228 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400229
230 docs = ['A description of how to use this function\n\n']
231 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400232 required = ""
233 if arg in required_params:
234 required = " (required)"
235 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400236
237 setattr(method, '__doc__', ''.join(docs))
238 setattr(theclass, methodName, method)
239
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400240 def createNextMethod(theclass, methodName, methodDesc):
241
242 def method(self, previous):
243 """
244 Takes a single argument, 'body', which is the results
245 from the last call, and returns the next set of items
246 in the collection.
247
248 Returns None if there are no more items in
249 the collection.
250 """
251 if methodDesc['type'] != 'uri':
252 raise UnknownLinkType(methodDesc['type'])
253
254 try:
255 p = previous
256 for key in methodDesc['location']:
257 p = p[key]
258 url = p
259 except KeyError:
260 return None
261
262 headers = {}
263 headers, params, query, body = self._model.request(headers, {}, {}, None)
264
265 logging.info('URL being requested: %s' % url)
266 resp, content = self._http.request(url, method='GET', headers=headers)
267
268 return self._model.response(resp, content)
269
270 setattr(theclass, methodName, method)
271
272 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400273 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400274 createMethod(Resource, methodName, methodDesc, futureDesc['methods'].get(methodName, {}))
275
276 # Add <m>_next() methods to Resource
277 for methodName, methodDesc in futureDesc['methods'].iteritems():
278 if 'next' in methodDesc and methodName in resourceDesc['methods']:
279 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400280
281 return Resource()