blob: f853c444955e95cd752a66cd41bcae6293bc4491 [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
ade@google.comc5eb46f2010-09-27 23:35:39 +010031try:
32 from urlparse import parse_qsl
33except ImportError:
34 from cgi import parse_qsl
Joe Gregorio5f087cf2010-09-20 16:08:07 -040035from apiclient.http import HttpRequest
Joe Gregoriof658e322010-10-11 15:40:31 -040036from apiclient.json import simplejson
Joe Gregorio48d361f2010-08-18 13:19:21 -040037
Tom Miller05cd4f52010-10-06 11:09:12 -070038class Error(Exception):
39 """Base error for this module."""
Joe Gregorio41cf7972010-08-18 15:21:06 -040040 pass
41
Joe Gregorio3bbbf662010-08-30 16:41:53 -040042
Tom Miller05cd4f52010-10-06 11:09:12 -070043class HttpError(Error):
44 """HTTP data was invalid or unexpected."""
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040045 pass
46
Tom Miller05cd4f52010-10-06 11:09:12 -070047
48class UnknownLinkType(Error):
49 """Link type unknown or unexpected."""
50 pass
51
52
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040053DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe'
54 '{?api,apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040055
56
Joe Gregorio48d361f2010-08-18 13:19:21 -040057def key2param(key):
58 """
59 max-results -> max_results
60 """
61 result = []
62 key = list(key)
63 if not key[0].isalpha():
64 result.append('x')
65 for c in key:
66 if c.isalnum():
67 result.append(c)
68 else:
69 result.append('_')
70
71 return ''.join(result)
72
73
74class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040075
ade@google.com850cf552010-08-20 23:24:56 +010076 def request(self, headers, path_params, query_params, body_value):
77 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040078 headers['accept'] = 'application/json'
Joe Gregorio6b2e5dc2010-09-14 15:11:03 -040079 if 'user-agent' in headers:
80 headers['user-agent'] += ' '
81 else:
82 headers['user-agent'] = ''
Joe Gregorioe9369582010-09-14 15:17:28 -040083 headers['user-agent'] += 'google-api-python-client/1.0'
ade@google.com850cf552010-08-20 23:24:56 +010084 if body_value is None:
85 return (headers, path_params, query, None)
Joe Gregorio48d361f2010-08-18 13:19:21 -040086 else:
Joe Gregorio46b0ff62010-10-09 22:13:12 -040087 if len(body_value) == 1 and 'data' in body_value:
88 model = body_value
89 else:
90 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040091 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010092 return (headers, path_params, query, simplejson.dumps(model))
93
94 def build_query(self, params):
jcgregorio@google.come3c8b6d2010-10-07 19:34:54 -040095 params.update({'alt': 'json'})
Joe Gregoriofe695fb2010-08-30 12:04:04 -040096 astuples = []
97 for key, value in params.iteritems():
98 if getattr(value, 'encode', False) and callable(value.encode):
99 value = value.encode('utf-8')
100 astuples.append((key, value))
101 return '?' + urllib.urlencode(astuples)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400102
103 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400104 # Error handling is TBD, for example, do we retry
105 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106 if resp.status < 300:
ade@google.com9d821b02010-10-11 03:33:51 -0700107 if resp.status == 204:
108 # A 204: No Content response should be treated differently to all the other success states
109 return simplejson.loads('{}')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400110 return simplejson.loads(content)['data']
111 else:
ade@google.com850cf552010-08-20 23:24:56 +0100112 logging.debug('Content from bad request was: %s' % content)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400113 if resp.get('content-type', '') != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400114 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400115 else:
116 raise HttpError(simplejson.loads(content)['error'])
117
118
Joe Gregorioc204b642010-09-21 12:01:23 -0400119def build(serviceName, version, http=None,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400120 discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400121 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400122 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400123 'apiVersion': version
124 }
ade@google.com850cf552010-08-20 23:24:56 +0100125
Joe Gregorioc204b642010-09-21 12:01:23 -0400126 if http is None:
127 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100128 requested_url = uritemplate.expand(discoveryServiceUrl, params)
129 logging.info('URL being requested: %s' % requested_url)
130 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400131 d = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400132 service = d['data'][serviceName][version]
133
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400134 fn = os.path.join(os.path.dirname(__file__), "contrib",
135 serviceName, "future.json")
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400136 f = file(fn, "r")
137 d = simplejson.load(f)
138 f.close()
139 future = d['data'][serviceName][version]['resources']
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400140 auth_discovery = d['data'][serviceName][version]['auth']
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400141
Joe Gregorio48d361f2010-08-18 13:19:21 -0400142 base = service['baseUrl']
143 resources = service['resources']
144
145 class Service(object):
146 """Top level interface for a service"""
147
148 def __init__(self, http=http):
149 self._http = http
150 self._baseUrl = base
151 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400152 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400153
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400154 def auth_discovery(self):
155 return auth_discovery
156
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400157 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400158
Joe Gregorio59cd9512010-10-04 12:46:46 -0400159 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400160 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400161 methodName, self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400162
163 setattr(method, '__doc__', 'A description of how to use this function')
164 setattr(theclass, methodName, method)
165
166 for methodName, methodDesc in resources.iteritems():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400167 createMethod(Service, methodName, methodDesc, future[methodName])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400168 return Service()
169
170
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400171def createResource(http, baseUrl, model, resourceName, developerKey,
172 resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400173
174 class Resource(object):
175 """A class for interacting with a resource."""
176
177 def __init__(self):
178 self._http = http
179 self._baseUrl = baseUrl
180 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400181 self._developerKey = developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400182
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400183 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400184 pathUrl = methodDesc['pathUrl']
185 pathUrl = re.sub(r'\{', r'{+', pathUrl)
186 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400187
ade@google.com850cf552010-08-20 23:24:56 +0100188 argmap = {}
189 if httpMethod in ['PUT', 'POST']:
190 argmap['body'] = 'body'
191
192
193 required_params = [] # Required parameters
194 pattern_params = {} # Parameters that must match a regex
195 query_params = [] # Parameters that will be used in the query string
196 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400197 if 'parameters' in methodDesc:
198 for arg, desc in methodDesc['parameters'].iteritems():
199 param = key2param(arg)
200 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400201
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400202 if desc.get('pattern', ''):
203 pattern_params[param] = desc['pattern']
204 if desc.get('required', False):
205 required_params.append(param)
206 if desc.get('parameterType') == 'query':
207 query_params.append(param)
208 if desc.get('parameterType') == 'path':
209 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400210
211 def method(self, **kwargs):
212 for name in kwargs.iterkeys():
213 if name not in argmap:
214 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400215
ade@google.com850cf552010-08-20 23:24:56 +0100216 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400217 if name not in kwargs:
218 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400219
ade@google.com850cf552010-08-20 23:24:56 +0100220 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400221 if name in kwargs:
222 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400223 raise TypeError(
224 'Parameter "%s" value "%s" does not match the pattern "%s"' %
225 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400226
ade@google.com850cf552010-08-20 23:24:56 +0100227 actual_query_params = {}
228 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400229 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100230 if key in query_params:
231 actual_query_params[argmap[key]] = value
232 if key in path_params:
233 actual_path_params[argmap[key]] = value
234 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400235
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400236 if self._developerKey:
237 actual_query_params['key'] = self._developerKey
238
Joe Gregorio48d361f2010-08-18 13:19:21 -0400239 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400240 headers, params, query, body = self._model.request(headers,
241 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400242
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100243 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
244 # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
245 # This results in an incorrect URL which returns a 404
246 url_result = urlparse.urlsplit(self._baseUrl)
247 new_base_url = url_result.scheme + '://' + url_result.netloc
248
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400249 expanded_url = uritemplate.expand(pathUrl, params)
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100250 url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400251
ade@google.com850cf552010-08-20 23:24:56 +0100252 logging.info('URL being requested: %s' % url)
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400253 return HttpRequest(self._http, url, method=httpMethod, body=body,
254 headers=headers, postproc=self._model.response)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400255
256 docs = ['A description of how to use this function\n\n']
257 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400258 required = ""
259 if arg in required_params:
260 required = " (required)"
261 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400262
263 setattr(method, '__doc__', ''.join(docs))
264 setattr(theclass, methodName, method)
265
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400266 def createNextMethod(theclass, methodName, methodDesc):
267
268 def method(self, previous):
269 """
270 Takes a single argument, 'body', which is the results
271 from the last call, and returns the next set of items
272 in the collection.
273
274 Returns None if there are no more items in
275 the collection.
276 """
277 if methodDesc['type'] != 'uri':
278 raise UnknownLinkType(methodDesc['type'])
279
280 try:
281 p = previous
282 for key in methodDesc['location']:
283 p = p[key]
284 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400285 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400286 return None
287
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400288 if self._developerKey:
289 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100290 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400291 q.append(('key', self._developerKey))
292 parsed[4] = urllib.urlencode(q)
293 url = urlparse.urlunparse(parsed)
294
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400295 headers = {}
296 headers, params, query, body = self._model.request(headers, {}, {}, None)
297
298 logging.info('URL being requested: %s' % url)
299 resp, content = self._http.request(url, method='GET', headers=headers)
300
Joe Gregorio5f087cf2010-09-20 16:08:07 -0400301 return HttpRequest(self._http, url, method='GET',
302 headers=headers, postproc=self._model.response)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400303
304 setattr(theclass, methodName, method)
305
306 # Add basic methods to Resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400307 for methodName, methodDesc in resourceDesc['methods'].iteritems():
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400308 future = futureDesc['methods'].get(methodName, {})
309 createMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400310
311 # Add <m>_next() methods to Resource
312 for methodName, methodDesc in futureDesc['methods'].iteritems():
313 if 'next' in methodDesc and methodName in resourceDesc['methods']:
314 createNextMethod(Resource, methodName + "_next", methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400315
316 return Resource()