blob: 36d38e7f2acb384f44d6a424dd574aef5872262b [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 Gregorioaf276d22010-12-09 14:26:58 -050035
Joe Gregorio5f087cf2010-09-20 16:08:07 -040036from apiclient.http import HttpRequest
Joe Gregoriof658e322010-10-11 15:40:31 -040037from apiclient.json import simplejson
Joe Gregorioaf276d22010-12-09 14:26:58 -050038from apiclient.model import JsonModel
39from apiclient.errors import HttpError
40from apiclient.errors import UnknownLinkType
Joe Gregorio48d361f2010-08-18 13:19:21 -040041
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050042URITEMPLATE = re.compile('{[^}]*}')
43VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio2379ecc2010-10-26 10:51:28 -040044DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
45 '{api}/{apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040046
47
Joe Gregorio48d361f2010-08-18 13:19:21 -040048def key2param(key):
49 """
50 max-results -> max_results
51 """
52 result = []
53 key = list(key)
54 if not key[0].isalpha():
55 result.append('x')
56 for c in key:
57 if c.isalnum():
58 result.append(c)
59 else:
60 result.append('_')
61
62 return ''.join(result)
63
64
Joe Gregorioaf276d22010-12-09 14:26:58 -050065def build(serviceName, version,
66 http=None,
67 discoveryServiceUrl=DISCOVERY_URI,
68 developerKey=None,
69 model=JsonModel(),
70 requestBuilder=HttpRequest):
Joe Gregorio48d361f2010-08-18 13:19:21 -040071 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040072 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -040073 'apiVersion': version
74 }
ade@google.com850cf552010-08-20 23:24:56 +010075
Joe Gregorioc204b642010-09-21 12:01:23 -040076 if http is None:
77 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +010078 requested_url = uritemplate.expand(discoveryServiceUrl, params)
79 logging.info('URL being requested: %s' % requested_url)
80 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -040081 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040082
Joe Gregorio3bbbf662010-08-30 16:41:53 -040083 fn = os.path.join(os.path.dirname(__file__), "contrib",
84 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -040085 try:
86 f = file(fn, "r")
87 d = simplejson.load(f)
88 f.close()
89 future = d['resources']
90 auth_discovery = d['auth']
91 except IOError:
92 future = {}
93 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040094
Joe Gregorio2379ecc2010-10-26 10:51:28 -040095 base = urlparse.urljoin(discoveryServiceUrl, service['restBasePath'])
Joe Gregorio48d361f2010-08-18 13:19:21 -040096 resources = service['resources']
97
98 class Service(object):
99 """Top level interface for a service"""
100
101 def __init__(self, http=http):
102 self._http = http
103 self._baseUrl = base
104 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400105 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500106 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400107
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400108 def auth_discovery(self):
109 return auth_discovery
110
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400111 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400112
Joe Gregorio59cd9512010-10-04 12:46:46 -0400113 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500115 self._requestBuilder, methodName,
116 self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400117
118 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400119 setattr(method, '__is_resource__', True)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400120 setattr(theclass, methodName, method)
121
122 for methodName, methodDesc in resources.iteritems():
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400123 createMethod(Service, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400124 return Service()
125
126
Joe Gregorioaf276d22010-12-09 14:26:58 -0500127def createResource(http, baseUrl, model, requestBuilder, resourceName,
128 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400129
130 class Resource(object):
131 """A class for interacting with a resource."""
132
133 def __init__(self):
134 self._http = http
135 self._baseUrl = baseUrl
136 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400137 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500138 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400139
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400140 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400141 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400142 pathUrl = re.sub(r'\{', r'{+', pathUrl)
143 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500144 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400145
ade@google.com850cf552010-08-20 23:24:56 +0100146 argmap = {}
147 if httpMethod in ['PUT', 'POST']:
148 argmap['body'] = 'body'
149
150
151 required_params = [] # Required parameters
152 pattern_params = {} # Parameters that must match a regex
153 query_params = [] # Parameters that will be used in the query string
154 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400155 if 'parameters' in methodDesc:
156 for arg, desc in methodDesc['parameters'].iteritems():
157 param = key2param(arg)
158 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400159
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400160 if desc.get('pattern', ''):
161 pattern_params[param] = desc['pattern']
162 if desc.get('required', False):
163 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400164 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400165 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400166 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400167 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400168
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500169 for match in URITEMPLATE.finditer(pathUrl):
170 for namematch in VARNAME.finditer(match.group(0)):
171 name = key2param(namematch.group(0))
172 path_params[name] = name
173 if name in query_params:
174 query_params.remove(name)
175
Joe Gregorio48d361f2010-08-18 13:19:21 -0400176 def method(self, **kwargs):
177 for name in kwargs.iterkeys():
178 if name not in argmap:
179 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400180
ade@google.com850cf552010-08-20 23:24:56 +0100181 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400182 if name not in kwargs:
183 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400184
ade@google.com850cf552010-08-20 23:24:56 +0100185 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400186 if name in kwargs:
187 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400188 raise TypeError(
189 'Parameter "%s" value "%s" does not match the pattern "%s"' %
190 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400191
ade@google.com850cf552010-08-20 23:24:56 +0100192 actual_query_params = {}
193 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400194 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100195 if key in query_params:
196 actual_query_params[argmap[key]] = value
197 if key in path_params:
198 actual_path_params[argmap[key]] = value
199 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400200
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400201 if self._developerKey:
202 actual_query_params['key'] = self._developerKey
203
Joe Gregorio48d361f2010-08-18 13:19:21 -0400204 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400205 headers, params, query, body = self._model.request(headers,
206 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400207
Joe Gregorioaf276d22010-12-09 14:26:58 -0500208 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
209 # document. Base URLs should not contain any path elements. If they do
210 # then urlparse.urljoin will strip them out This results in an incorrect
211 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100212 url_result = urlparse.urlsplit(self._baseUrl)
213 new_base_url = url_result.scheme + '://' + url_result.netloc
214
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400215 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500216 url = urlparse.urljoin(new_base_url,
217 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400218
ade@google.com850cf552010-08-20 23:24:56 +0100219 logging.info('URL being requested: %s' % url)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500220 return self._requestBuilder(self._http, url,
221 method=httpMethod, body=body,
222 headers=headers,
223 postproc=self._model.response,
224 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400225
226 docs = ['A description of how to use this function\n\n']
227 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400228 required = ""
229 if arg in required_params:
230 required = " (required)"
231 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400232
233 setattr(method, '__doc__', ''.join(docs))
234 setattr(theclass, methodName, method)
235
Joe Gregorioaf276d22010-12-09 14:26:58 -0500236 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
237 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400238
239 def method(self, previous):
240 """
241 Takes a single argument, 'body', which is the results
242 from the last call, and returns the next set of items
243 in the collection.
244
245 Returns None if there are no more items in
246 the collection.
247 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500248 if futureDesc['type'] != 'uri':
249 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400250
251 try:
252 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500253 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400254 p = p[key]
255 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400256 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400257 return None
258
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400259 if self._developerKey:
260 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100261 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400262 q.append(('key', self._developerKey))
263 parsed[4] = urllib.urlencode(q)
264 url = urlparse.urlunparse(parsed)
265
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400266 headers = {}
267 headers, params, query, body = self._model.request(headers, {}, {}, None)
268
269 logging.info('URL being requested: %s' % url)
270 resp, content = self._http.request(url, method='GET', headers=headers)
271
Joe Gregorioaf276d22010-12-09 14:26:58 -0500272 return self._requestBuilder(self._http, url, method='GET',
273 headers=headers,
274 postproc=self._model.response,
275 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400276
277 setattr(theclass, methodName, method)
278
279 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400280 if 'methods' in resourceDesc:
281 for methodName, methodDesc in resourceDesc['methods'].iteritems():
282 if futureDesc:
283 future = futureDesc['methods'].get(methodName, {})
284 else:
285 future = None
286 createMethod(Resource, methodName, methodDesc, future)
287
288 # Add in nested resources
289 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500290
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400291 def createMethod(theclass, methodName, methodDesc, futureDesc):
292
293 def method(self):
294 return createResource(self._http, self._baseUrl, self._model,
295 methodName, self._developerKey, methodDesc, futureDesc)
296
297 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400298 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400299 setattr(theclass, methodName, method)
300
301 for methodName, methodDesc in resourceDesc['resources'].iteritems():
302 if futureDesc and 'resources' in futureDesc:
303 future = futureDesc['resources'].get(methodName, {})
304 else:
305 future = {}
Joe Gregorioaf276d22010-12-09 14:26:58 -0500306 createMethod(Resource, methodName, methodDesc,
307 future.get(methodName, {}))
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400308
309 # Add <m>_next() methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400310 if futureDesc:
311 for methodName, methodDesc in futureDesc['methods'].iteritems():
312 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500313 createNextMethod(Resource, methodName + "_next",
314 resourceDesc['methods'][methodName],
315 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400316
317 return Resource()