blob: b859a7d9863f442f717cde5c68b218f3c5640f14 [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 Gregorio21f11672010-08-18 17:23:17 -0400166 for arg, desc in methodDesc['parameters'].iteritems():
167 param = key2param(arg)
ade@google.com850cf552010-08-20 23:24:56 +0100168 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400169
ade@google.com850cf552010-08-20 23:24:56 +0100170 if desc.get('pattern', ''):
171 pattern_params[param] = desc['pattern']
172 if desc.get('required', False):
173 required_params.append(param)
174 if desc.get('parameterType') == 'query':
175 query_params.append(param)
176 if desc.get('parameterType') == 'path':
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400177 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400178
179 def method(self, **kwargs):
180 for name in kwargs.iterkeys():
181 if name not in argmap:
182 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400183
ade@google.com850cf552010-08-20 23:24:56 +0100184 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400185 if name not in kwargs:
186 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400187
ade@google.com850cf552010-08-20 23:24:56 +0100188 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400189 if name in kwargs:
190 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400191 raise TypeError(
192 'Parameter "%s" value "%s" does not match the pattern "%s"' %
193 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400194
ade@google.com850cf552010-08-20 23:24:56 +0100195 actual_query_params = {}
196 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400197 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100198 if key in query_params:
199 actual_query_params[argmap[key]] = value
200 if key in path_params:
201 actual_path_params[argmap[key]] = value
202 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400203
Joe Gregorio48d361f2010-08-18 13:19:21 -0400204 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400205 headers, params, query, body = self._model.request(headers,
206 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400207
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400208 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100209 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400210
ade@google.com850cf552010-08-20 23:24:56 +0100211 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400212 resp, content = self._http.request(
213 url, method=httpMethod, headers=headers, body=body)
214
215 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400216
217 docs = ['A description of how to use this function\n\n']
218 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400219 required = ""
220 if arg in required_params:
221 required = " (required)"
222 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400223
224 setattr(method, '__doc__', ''.join(docs))
225 setattr(theclass, methodName, method)
226
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400227 def createNextMethod(theclass, methodName, methodDesc):
228
229 def method(self, previous):
230 """
231 Takes a single argument, 'body', which is the results
232 from the last call, and returns the next set of items
233 in the collection.
234
235 Returns None if there are no more items in
236 the collection.
237 """
238 if methodDesc['type'] != 'uri':
239 raise UnknownLinkType(methodDesc['type'])
240
241 try:
242 p = previous
243 for key in methodDesc['location']:
244 p = p[key]
245 url = p
246 except KeyError:
247 return None
248
249 headers = {}
250 headers, params, query, body = self._model.request(headers, {}, {}, None)
251
252 logging.info('URL being requested: %s' % url)
253 resp, content = self._http.request(url, method='GET', headers=headers)
254
255 return self._model.response(resp, content)
256
257 setattr(theclass, methodName, method)
258
259 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400260 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400261 future = futureDesc['methods'].get(methodName, {})
262 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400263
264 # Add <m>_next() methods to Resource
265 for methodName, methodDesc in futureDesc['methods'].iteritems():
266 if 'next' in methodDesc and methodName in resourceDesc['methods']:
267 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400268
269 return Resource()