blob: 5ae7f028a85aea2ba82e4f8e2cf94f9027e58021 [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']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400115 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400116
Joe Gregorio48d361f2010-08-18 13:19:21 -0400117 base = service['baseUrl']
118 resources = service['resources']
119
120 class Service(object):
121 """Top level interface for a service"""
122
123 def __init__(self, http=http):
124 self._http = http
125 self._baseUrl = base
126 self._model = model
127
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400128 def auth_discovery(self):
129 return auth_discovery
130
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400131 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400132
Joe Gregorio48d361f2010-08-18 13:19:21 -0400133 def method(self, **kwargs):
134 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400135 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400136
137 setattr(method, '__doc__', 'A description of how to use this function')
138 setattr(theclass, methodName, method)
139
140 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400141 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400142 return Service()
143
144
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400145def createResource(http, baseUrl, model, resourceName, resourceDesc,
146 futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400147
148 class Resource(object):
149 """A class for interacting with a resource."""
150
151 def __init__(self):
152 self._http = http
153 self._baseUrl = baseUrl
154 self._model = model
155
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400156 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400157 pathUrl = methodDesc['pathUrl']
158 pathUrl = re.sub(r'\{', r'{+', pathUrl)
159 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400160
ade@google.com850cf552010-08-20 23:24:56 +0100161 argmap = {}
162 if httpMethod in ['PUT', 'POST']:
163 argmap['body'] = 'body'
164
165
166 required_params = [] # Required parameters
167 pattern_params = {} # Parameters that must match a regex
168 query_params = [] # Parameters that will be used in the query string
169 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400170 if 'parameters' in methodDesc:
171 for arg, desc in methodDesc['parameters'].iteritems():
172 param = key2param(arg)
173 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400174
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400175 if desc.get('pattern', ''):
176 pattern_params[param] = desc['pattern']
177 if desc.get('required', False):
178 required_params.append(param)
179 if desc.get('parameterType') == 'query':
180 query_params.append(param)
181 if desc.get('parameterType') == 'path':
182 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400183
184 def method(self, **kwargs):
185 for name in kwargs.iterkeys():
186 if name not in argmap:
187 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400188
ade@google.com850cf552010-08-20 23:24:56 +0100189 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400190 if name not in kwargs:
191 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400192
ade@google.com850cf552010-08-20 23:24:56 +0100193 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400194 if name in kwargs:
195 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400196 raise TypeError(
197 'Parameter "%s" value "%s" does not match the pattern "%s"' %
198 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400199
ade@google.com850cf552010-08-20 23:24:56 +0100200 actual_query_params = {}
201 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400202 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100203 if key in query_params:
204 actual_query_params[argmap[key]] = value
205 if key in path_params:
206 actual_path_params[argmap[key]] = value
207 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400208
Joe Gregorio48d361f2010-08-18 13:19:21 -0400209 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400210 headers, params, query, body = self._model.request(headers,
211 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400212
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400213 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100214 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400215
ade@google.com850cf552010-08-20 23:24:56 +0100216 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400217 resp, content = self._http.request(
218 url, method=httpMethod, headers=headers, body=body)
219
220 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400221
222 docs = ['A description of how to use this function\n\n']
223 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400224 required = ""
225 if arg in required_params:
226 required = " (required)"
227 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400228
229 setattr(method, '__doc__', ''.join(docs))
230 setattr(theclass, methodName, method)
231
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400232 def createNextMethod(theclass, methodName, methodDesc):
233
234 def method(self, previous):
235 """
236 Takes a single argument, 'body', which is the results
237 from the last call, and returns the next set of items
238 in the collection.
239
240 Returns None if there are no more items in
241 the collection.
242 """
243 if methodDesc['type'] != 'uri':
244 raise UnknownLinkType(methodDesc['type'])
245
246 try:
247 p = previous
248 for key in methodDesc['location']:
249 p = p[key]
250 url = p
251 except KeyError:
252 return None
253
254 headers = {}
255 headers, params, query, body = self._model.request(headers, {}, {}, None)
256
257 logging.info('URL being requested: %s' % url)
258 resp, content = self._http.request(url, method='GET', headers=headers)
259
260 return self._model.response(resp, content)
261
262 setattr(theclass, methodName, method)
263
264 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400265 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400266 future = futureDesc['methods'].get(methodName, {})
267 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400268
269 # Add <m>_next() methods to Resource
270 for methodName, methodDesc in futureDesc['methods'].iteritems():
271 if 'next' in methodDesc and methodName in resourceDesc['methods']:
272 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400273
274 return Resource()