blob: cbba1dccc3b8c4642bb042e9dc1dabef93fd6d31 [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
Joe Gregorio48d361f2010-08-18 13:19:21 -040028import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040029import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040030import urlparse
Joe Gregorio5f087cf2010-09-20 16:08:07 -040031from apiclient.http import HttpRequest
Joe Gregorio48d361f2010-08-18 13:19:21 -040032
Joe Gregorioe6efd532010-09-20 11:13:50 -040033try:
34 import simplejson
35except ImportError:
36 try:
37 # Try to import from django, should work on App Engine
38 from django.utils import simplejson
39 except ImportError:
40 # Should work for Python2.6 and higher.
41 import json as simplejson
42
Joe Gregorio48d361f2010-08-18 13:19:21 -040043
Joe Gregorio41cf7972010-08-18 15:21:06 -040044class HttpError(Exception):
45 pass
46
Joe Gregorio3bbbf662010-08-30 16:41:53 -040047
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040048class UnknownLinkType(Exception):
49 pass
50
51DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
52 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040053
54
Joe Gregorio48d361f2010-08-18 13:19:21 -040055def key2param(key):
56 """
57 max-results -> max_results
58 """
59 result = []
60 key = list(key)
61 if not key[0].isalpha():
62 result.append('x')
63 for c in key:
64 if c.isalnum():
65 result.append(c)
66 else:
67 result.append('_')
68
69 return ''.join(result)
70
71
72class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040073
ade@google.com850cf552010-08-20 23:24:56 +010074 def request(self, headers, path_params, query_params, body_value):
75 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040076 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040077 if 'user-agent' in headers:
78 headers['user-agent'] += ' '
79 else:
80 headers['user-agent'] = ''
Joe Gregorioe9369582010-09-14 15:17:28 -040081 headers['user-agent'] += 'google-api-python-client/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010082 if body_value is None:
83 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040084 else:
ade@google.com850cf552010-08-20 23:24:56 +010085 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040086 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010087 return (headers, path_params, query, simplejson.dumps(model))
88
89 def build_query(self, params):
Joe Gregoriofe695fb2010-08-30 12:04:04 -040090 params.update({'alt': 'json', 'prettyprint': 'true'})
91 astuples = []
92 for key, value in params.iteritems():
93 if getattr(value, 'encode', False) and callable(value.encode):
94 value = value.encode('utf-8')
95 astuples.append((key, value))
96 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -040097
98 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040099 # Error handling is TBD, for example, do we retry
100 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400101 if resp.status < 300:
102 return simplejson.loads(content)['data']
103 else:
ade@google.com850cf552010-08-20 23:24:56 +0100104 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400105 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400106 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400107 else:
108 raise HttpError(simplejson.loads(content)['error'])
109
110
Joe Gregorioc204b642010-09-21 12:01:23 -0400111def build(serviceName, version, http=None,
Joe Gregorio41cf7972010-08-18 15:21:06 -0400112 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400113 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400114 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400115 'apiVersion': version
116 }
ade@google.com850cf552010-08-20 23:24:56 +0100117
Joe Gregorioc204b642010-09-21 12:01:23 -0400118 if http is None:
119 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100120 requested_url = uritemplate.expand(discoveryServiceUrl, params)
121 logging.info('URL being requested: %s' % requested_url)
122 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400123 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400124 service = d['data'][serviceName][version]
125
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400126 fn = os.path.join(os.path.dirname(__file__), "contrib",
127 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400128 f = file(fn, "r")
129 d = simplejson.load(f)
130 f.close()
131 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400132 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400133
Joe Gregorio48d361f2010-08-18 13:19:21 -0400134 base = service['baseUrl']
135 resources = service['resources']
136
137 class Service(object):
138 """Top level interface for a service"""
139
140 def __init__(self, http=http):
141 self._http = http
142 self._baseUrl = base
143 self._model = model
144
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400145 def auth_discovery(self):
146 return auth_discovery
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 Gregorio3bbbf662010-08-30 16:41:53 -0400162def createResource(http, baseUrl, model, resourceName, resourceDesc,
163 futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400164
165 class Resource(object):
166 """A class for interacting with a resource."""
167
168 def __init__(self):
169 self._http = http
170 self._baseUrl = baseUrl
171 self._model = model
172
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400173 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400174 pathUrl = methodDesc['pathUrl']
175 pathUrl = re.sub(r'\{', r'{+', pathUrl)
176 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400177
ade@google.com850cf552010-08-20 23:24:56 +0100178 argmap = {}
179 if httpMethod in ['PUT', 'POST']:
180 argmap['body'] = 'body'
181
182
183 required_params = [] # Required parameters
184 pattern_params = {} # Parameters that must match a regex
185 query_params = [] # Parameters that will be used in the query string
186 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400187 if 'parameters' in methodDesc:
188 for arg, desc in methodDesc['parameters'].iteritems():
189 param = key2param(arg)
190 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400191
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400192 if desc.get('pattern', ''):
193 pattern_params[param] = desc['pattern']
194 if desc.get('required', False):
195 required_params.append(param)
196 if desc.get('parameterType') == 'query':
197 query_params.append(param)
198 if desc.get('parameterType') == 'path':
199 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400200
201 def method(self, **kwargs):
202 for name in kwargs.iterkeys():
203 if name not in argmap:
204 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400205
ade@google.com850cf552010-08-20 23:24:56 +0100206 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400207 if name not in kwargs:
208 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400209
ade@google.com850cf552010-08-20 23:24:56 +0100210 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400211 if name in kwargs:
212 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400213 raise TypeError(
214 'Parameter "%s" value "%s" does not match the pattern "%s"' %
215 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400216
ade@google.com850cf552010-08-20 23:24:56 +0100217 actual_query_params = {}
218 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400219 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100220 if key in query_params:
221 actual_query_params[argmap[key]] = value
222 if key in path_params:
223 actual_path_params[argmap[key]] = value
224 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400225
Joe Gregorio48d361f2010-08-18 13:19:21 -0400226 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400227 headers, params, query, body = self._model.request(headers,
228 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400229
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400230 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100231 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400232
ade@google.com850cf552010-08-20 23:24:56 +0100233 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400234 return HttpRequest(self._http, url, method=httpMethod, body=body,
235 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400236
237 docs = ['A description of how to use this function\n\n']
238 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400239 required = ""
240 if arg in required_params:
241 required = " (required)"
242 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400243
244 setattr(method, '__doc__', ''.join(docs))
245 setattr(theclass, methodName, method)
246
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400247 def createNextMethod(theclass, methodName, methodDesc):
248
249 def method(self, previous):
250 """
251 Takes a single argument, 'body', which is the results
252 from the last call, and returns the next set of items
253 in the collection.
254
255 Returns None if there are no more items in
256 the collection.
257 """
258 if methodDesc['type'] != 'uri':
259 raise UnknownLinkType(methodDesc['type'])
260
261 try:
262 p = previous
263 for key in methodDesc['location']:
264 p = p[key]
265 url = p
266 except KeyError:
267 return None
268
269 headers = {}
270 headers, params, query, body = self._model.request(headers, {}, {}, None)
271
272 logging.info('URL being requested: %s' % url)
273 resp, content = self._http.request(url, method='GET', headers=headers)
274
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400275 return HttpRequest(self._http, url, method='GET',
276 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400277
278 setattr(theclass, methodName, method)
279
280 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400281 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400282 future = futureDesc['methods'].get(methodName, {})
283 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400284
285 # Add <m>_next() methods to Resource
286 for methodName, methodDesc in futureDesc['methods'].iteritems():
287 if 'next' in methodDesc and methodName in resourceDesc['methods']:
288 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400289
290 return Resource()