blob: 408abd6e1b53aa59c07318d33dfe73b03d13b0cf [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'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040067 if 'user-agent' in headers:
68 headers['user-agent'] += ' '
69 else:
70 headers['user-agent'] = ''
71 headers['user-agent'] += 'google-api-client-python/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010072 if body_value is None:
73 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040074 else:
ade@google.com850cf552010-08-20 23:24:56 +010075 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040076 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010077 return (headers, path_params, query, simplejson.dumps(model))
78
79 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040080 params.update({'alt': 'json', 'prettyprint': 'true'})
81 astuples = []
82 for key, value in params.iteritems():
83 if getattr(value, 'encode', False) and callable(value.encode):
84 value = value.encode('utf-8')
85 astuples.append((key, value))
86 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -040087
88 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040089 # Error handling is TBD, for example, do we retry
90 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -040091 if resp.status < 300:
92 return simplejson.loads(content)['data']
93 else:
ade@google.com850cf552010-08-20 23:24:56 +010094 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -040095 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -040096 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -040097 else:
98 raise HttpError(simplejson.loads(content)['error'])
99
100
Joe Gregorioc204b642010-09-21 12:01:23 -0400101def build(serviceName, version, http=None,
Joe Gregorio41cf7972010-08-18 15:21:06 -0400102 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400103 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400104 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400105 'apiVersion': version
106 }
ade@google.com850cf552010-08-20 23:24:56 +0100107
Joe Gregorioc204b642010-09-21 12:01:23 -0400108 if http is None:
109 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100110 requested_url = uritemplate.expand(discoveryServiceUrl, params)
111 logging.info('URL being requested: %s' % requested_url)
112 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400113 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400114 service = d['data'][serviceName][version]
115
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400116 fn = os.path.join(os.path.dirname(__file__), "contrib",
117 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400118 f = file(fn, "r")
119 d = simplejson.load(f)
120 f.close()
121 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400122 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400123
Joe Gregorio48d361f2010-08-18 13:19:21 -0400124 base = service['baseUrl']
125 resources = service['resources']
126
127 class Service(object):
128 """Top level interface for a service"""
129
130 def __init__(self, http=http):
131 self._http = http
132 self._baseUrl = base
133 self._model = model
134
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400135 def auth_discovery(self):
136 return auth_discovery
137
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400138 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400139
Joe Gregorio48d361f2010-08-18 13:19:21 -0400140 def method(self, **kwargs):
141 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400142 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400143
144 setattr(method, '__doc__', 'A description of how to use this function')
145 setattr(theclass, methodName, method)
146
147 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400148 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400149 return Service()
150
151
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400152def createResource(http, baseUrl, model, resourceName, resourceDesc,
153 futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400154
155 class Resource(object):
156 """A class for interacting with a resource."""
157
158 def __init__(self):
159 self._http = http
160 self._baseUrl = baseUrl
161 self._model = model
162
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400163 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400164 pathUrl = methodDesc['pathUrl']
165 pathUrl = re.sub(r'\{', r'{+', pathUrl)
166 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400167
ade@google.com850cf552010-08-20 23:24:56 +0100168 argmap = {}
169 if httpMethod in ['PUT', 'POST']:
170 argmap['body'] = 'body'
171
172
173 required_params = [] # Required parameters
174 pattern_params = {} # Parameters that must match a regex
175 query_params = [] # Parameters that will be used in the query string
176 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400177 if 'parameters' in methodDesc:
178 for arg, desc in methodDesc['parameters'].iteritems():
179 param = key2param(arg)
180 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400181
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400182 if desc.get('pattern', ''):
183 pattern_params[param] = desc['pattern']
184 if desc.get('required', False):
185 required_params.append(param)
186 if desc.get('parameterType') == 'query':
187 query_params.append(param)
188 if desc.get('parameterType') == 'path':
189 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400190
191 def method(self, **kwargs):
192 for name in kwargs.iterkeys():
193 if name not in argmap:
194 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400195
ade@google.com850cf552010-08-20 23:24:56 +0100196 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400197 if name not in kwargs:
198 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400199
ade@google.com850cf552010-08-20 23:24:56 +0100200 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400201 if name in kwargs:
202 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400203 raise TypeError(
204 'Parameter "%s" value "%s" does not match the pattern "%s"' %
205 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400206
ade@google.com850cf552010-08-20 23:24:56 +0100207 actual_query_params = {}
208 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400209 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100210 if key in query_params:
211 actual_query_params[argmap[key]] = value
212 if key in path_params:
213 actual_path_params[argmap[key]] = value
214 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400215
Joe Gregorio48d361f2010-08-18 13:19:21 -0400216 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400217 headers, params, query, body = self._model.request(headers,
218 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400219
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400220 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100221 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400222
ade@google.com850cf552010-08-20 23:24:56 +0100223 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400224 resp, content = self._http.request(
225 url, method=httpMethod, headers=headers, body=body)
226
227 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400228
229 docs = ['A description of how to use this function\n\n']
230 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400231 required = ""
232 if arg in required_params:
233 required = " (required)"
234 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400235
236 setattr(method, '__doc__', ''.join(docs))
237 setattr(theclass, methodName, method)
238
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400239 def createNextMethod(theclass, methodName, methodDesc):
240
241 def method(self, previous):
242 """
243 Takes a single argument, 'body', which is the results
244 from the last call, and returns the next set of items
245 in the collection.
246
247 Returns None if there are no more items in
248 the collection.
249 """
250 if methodDesc['type'] != 'uri':
251 raise UnknownLinkType(methodDesc['type'])
252
253 try:
254 p = previous
255 for key in methodDesc['location']:
256 p = p[key]
257 url = p
258 except KeyError:
259 return None
260
261 headers = {}
262 headers, params, query, body = self._model.request(headers, {}, {}, None)
263
264 logging.info('URL being requested: %s' % url)
265 resp, content = self._http.request(url, method='GET', headers=headers)
266
267 return self._model.response(resp, content)
268
269 setattr(theclass, methodName, method)
270
271 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400272 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400273 future = futureDesc['methods'].get(methodName, {})
274 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400275
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()