blob: 9ba279db3a116b9a6856b6ea7a36027d21ce1d71 [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 Gregorio6d5e94f2010-08-25 23:49:30 -0400111def build(serviceName, version, http=httplib2.Http(),
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
118 requested_url = uritemplate.expand(discoveryServiceUrl, params)
119 logging.info('URL being requested: %s' % requested_url)
120 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400121 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400122 service = d['data'][serviceName][version]
123
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400124 fn = os.path.join(os.path.dirname(__file__), "contrib",
125 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400126 f = file(fn, "r")
127 d = simplejson.load(f)
128 f.close()
129 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400130 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400131
Joe Gregorio48d361f2010-08-18 13:19:21 -0400132 base = service['baseUrl']
133 resources = service['resources']
134
135 class Service(object):
136 """Top level interface for a service"""
137
138 def __init__(self, http=http):
139 self._http = http
140 self._baseUrl = base
141 self._model = model
142
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400143 def auth_discovery(self):
144 return auth_discovery
145
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400146 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400147
Joe Gregorio48d361f2010-08-18 13:19:21 -0400148 def method(self, **kwargs):
149 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400150 methodName, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400151
152 setattr(method, '__doc__', 'A description of how to use this function')
153 setattr(theclass, methodName, method)
154
155 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400156 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400157 return Service()
158
159
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400160def createResource(http, baseUrl, model, resourceName, resourceDesc,
161 futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400162
163 class Resource(object):
164 """A class for interacting with a resource."""
165
166 def __init__(self):
167 self._http = http
168 self._baseUrl = baseUrl
169 self._model = model
170
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400171 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172 pathUrl = methodDesc['pathUrl']
173 pathUrl = re.sub(r'\{', r'{+', pathUrl)
174 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400175
ade@google.com850cf552010-08-20 23:24:56 +0100176 argmap = {}
177 if httpMethod in ['PUT', 'POST']:
178 argmap['body'] = 'body'
179
180
181 required_params = [] # Required parameters
182 pattern_params = {} # Parameters that must match a regex
183 query_params = [] # Parameters that will be used in the query string
184 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400185 if 'parameters' in methodDesc:
186 for arg, desc in methodDesc['parameters'].iteritems():
187 param = key2param(arg)
188 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400189
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400190 if desc.get('pattern', ''):
191 pattern_params[param] = desc['pattern']
192 if desc.get('required', False):
193 required_params.append(param)
194 if desc.get('parameterType') == 'query':
195 query_params.append(param)
196 if desc.get('parameterType') == 'path':
197 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400198
199 def method(self, **kwargs):
200 for name in kwargs.iterkeys():
201 if name not in argmap:
202 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400203
ade@google.com850cf552010-08-20 23:24:56 +0100204 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400205 if name not in kwargs:
206 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400207
ade@google.com850cf552010-08-20 23:24:56 +0100208 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400209 if name in kwargs:
210 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400211 raise TypeError(
212 'Parameter "%s" value "%s" does not match the pattern "%s"' %
213 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400214
ade@google.com850cf552010-08-20 23:24:56 +0100215 actual_query_params = {}
216 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400217 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100218 if key in query_params:
219 actual_query_params[argmap[key]] = value
220 if key in path_params:
221 actual_path_params[argmap[key]] = value
222 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400223
Joe Gregorio48d361f2010-08-18 13:19:21 -0400224 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400225 headers, params, query, body = self._model.request(headers,
226 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400227
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400228 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com850cf552010-08-20 23:24:56 +0100229 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400230
ade@google.com850cf552010-08-20 23:24:56 +0100231 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400232 return HttpRequest(self._http, url, method=httpMethod, body=body,
233 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400234
235 docs = ['A description of how to use this function\n\n']
236 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400237 required = ""
238 if arg in required_params:
239 required = " (required)"
240 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400241
242 setattr(method, '__doc__', ''.join(docs))
243 setattr(theclass, methodName, method)
244
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400245 def createNextMethod(theclass, methodName, methodDesc):
246
247 def method(self, previous):
248 """
249 Takes a single argument, 'body', which is the results
250 from the last call, and returns the next set of items
251 in the collection.
252
253 Returns None if there are no more items in
254 the collection.
255 """
256 if methodDesc['type'] != 'uri':
257 raise UnknownLinkType(methodDesc['type'])
258
259 try:
260 p = previous
261 for key in methodDesc['location']:
262 p = p[key]
263 url = p
264 except KeyError:
265 return None
266
267 headers = {}
268 headers, params, query, body = self._model.request(headers, {}, {}, None)
269
270 logging.info('URL being requested: %s' % url)
271 resp, content = self._http.request(url, method='GET', headers=headers)
272
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400273 return HttpRequest(self._http, url, method='GET',
274 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400275
276 setattr(theclass, methodName, method)
277
278 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400279 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400280 future = futureDesc['methods'].get(methodName, {})
281 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400282
283 # Add <m>_next() methods to Resource
284 for methodName, methodDesc in futureDesc['methods'].iteritems():
285 if 'next' in methodDesc and methodName in resourceDesc['methods']:
286 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400287
288 return Resource()