blob: fb9b7c5dba1e6d2418351cb9fd908c19ac4094ba [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 Gregoriob843fa22010-12-13 16:26:07 -050036from http import HttpRequest
Joe Gregorio034e7002010-12-15 08:45:03 -050037from anyjson import simplejson
Joe Gregoriob843fa22010-12-13 16:26:07 -050038from model import JsonModel
Joe Gregoriob843fa22010-12-13 16:26:07 -050039from errors import UnknownLinkType
Joe Gregorio48d361f2010-08-18 13:19:21 -040040
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050041URITEMPLATE = re.compile('{[^}]*}')
42VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio2379ecc2010-10-26 10:51:28 -040043DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
44 '{api}/{apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040045
46
Joe Gregorio48d361f2010-08-18 13:19:21 -040047def key2param(key):
48 """
49 max-results -> max_results
50 """
51 result = []
52 key = list(key)
53 if not key[0].isalpha():
54 result.append('x')
55 for c in key:
56 if c.isalnum():
57 result.append(c)
58 else:
59 result.append('_')
60
61 return ''.join(result)
62
63
Joe Gregorioaf276d22010-12-09 14:26:58 -050064def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050065 http=None,
66 discoveryServiceUrl=DISCOVERY_URI,
67 developerKey=None,
68 model=JsonModel(),
69 requestBuilder=HttpRequest):
Joe Gregorio48d361f2010-08-18 13:19:21 -040070 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040071 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -040072 'apiVersion': version
73 }
ade@google.com850cf552010-08-20 23:24:56 +010074
Joe Gregorioc204b642010-09-21 12:01:23 -040075 if http is None:
76 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +010077 requested_url = uritemplate.expand(discoveryServiceUrl, params)
78 logging.info('URL being requested: %s' % requested_url)
79 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -040080 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040081
Joe Gregorio3bbbf662010-08-30 16:41:53 -040082 fn = os.path.join(os.path.dirname(__file__), "contrib",
83 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -040084 try:
85 f = file(fn, "r")
86 d = simplejson.load(f)
87 f.close()
88 future = d['resources']
89 auth_discovery = d['auth']
90 except IOError:
91 future = {}
92 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040093
Joe Gregorio2379ecc2010-10-26 10:51:28 -040094 base = urlparse.urljoin(discoveryServiceUrl, service['restBasePath'])
Joe Gregorio48d361f2010-08-18 13:19:21 -040095 resources = service['resources']
96
97 class Service(object):
98 """Top level interface for a service"""
99
100 def __init__(self, http=http):
101 self._http = http
102 self._baseUrl = base
103 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400104 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500105 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400107 def auth_discovery(self):
108 return auth_discovery
109
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400110 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400111
Joe Gregorio59cd9512010-10-04 12:46:46 -0400112 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400113 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500114 self._requestBuilder, methodName,
115 self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400116
117 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400118 setattr(method, '__is_resource__', True)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400119 setattr(theclass, methodName, method)
120
121 for methodName, methodDesc in resources.iteritems():
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400122 createMethod(Service, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400123 return Service()
124
125
Joe Gregorioaf276d22010-12-09 14:26:58 -0500126def createResource(http, baseUrl, model, requestBuilder, resourceName,
127 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400128
129 class Resource(object):
130 """A class for interacting with a resource."""
131
132 def __init__(self):
133 self._http = http
134 self._baseUrl = baseUrl
135 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400136 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500137 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400138
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400139 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400140 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400141 pathUrl = re.sub(r'\{', r'{+', pathUrl)
142 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500143 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400144
ade@google.com850cf552010-08-20 23:24:56 +0100145 argmap = {}
146 if httpMethod in ['PUT', 'POST']:
147 argmap['body'] = 'body'
148
149
150 required_params = [] # Required parameters
151 pattern_params = {} # Parameters that must match a regex
152 query_params = [] # Parameters that will be used in the query string
153 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400154 if 'parameters' in methodDesc:
155 for arg, desc in methodDesc['parameters'].iteritems():
156 param = key2param(arg)
157 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400158
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400159 if desc.get('pattern', ''):
160 pattern_params[param] = desc['pattern']
161 if desc.get('required', False):
162 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400163 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400164 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400165 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400166 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400167
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500168 for match in URITEMPLATE.finditer(pathUrl):
169 for namematch in VARNAME.finditer(match.group(0)):
170 name = key2param(namematch.group(0))
171 path_params[name] = name
172 if name in query_params:
173 query_params.remove(name)
174
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175 def method(self, **kwargs):
176 for name in kwargs.iterkeys():
177 if name not in argmap:
178 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400179
ade@google.com850cf552010-08-20 23:24:56 +0100180 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400181 if name not in kwargs:
182 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400183
ade@google.com850cf552010-08-20 23:24:56 +0100184 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400185 if name in kwargs:
186 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400187 raise TypeError(
188 'Parameter "%s" value "%s" does not match the pattern "%s"' %
189 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400190
ade@google.com850cf552010-08-20 23:24:56 +0100191 actual_query_params = {}
192 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400193 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100194 if key in query_params:
195 actual_query_params[argmap[key]] = value
196 if key in path_params:
197 actual_path_params[argmap[key]] = value
198 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400199
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400200 if self._developerKey:
201 actual_query_params['key'] = self._developerKey
202
Joe Gregorio48d361f2010-08-18 13:19:21 -0400203 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400204 headers, params, query, body = self._model.request(headers,
205 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400206
Joe Gregorioaf276d22010-12-09 14:26:58 -0500207 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
208 # document. Base URLs should not contain any path elements. If they do
209 # then urlparse.urljoin will strip them out This results in an incorrect
210 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100211 url_result = urlparse.urlsplit(self._baseUrl)
212 new_base_url = url_result.scheme + '://' + url_result.netloc
213
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400214 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500215 url = urlparse.urljoin(new_base_url,
216 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400217
ade@google.com850cf552010-08-20 23:24:56 +0100218 logging.info('URL being requested: %s' % url)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500219 return self._requestBuilder(self._http, url,
220 method=httpMethod, body=body,
221 headers=headers,
222 postproc=self._model.response,
223 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400224
225 docs = ['A description of how to use this function\n\n']
226 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400227 required = ""
228 if arg in required_params:
229 required = " (required)"
230 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400231
232 setattr(method, '__doc__', ''.join(docs))
233 setattr(theclass, methodName, method)
234
Joe Gregorioaf276d22010-12-09 14:26:58 -0500235 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
236 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400237
238 def method(self, previous):
239 """
240 Takes a single argument, 'body', which is the results
241 from the last call, and returns the next set of items
242 in the collection.
243
244 Returns None if there are no more items in
245 the collection.
246 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500247 if futureDesc['type'] != 'uri':
248 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400249
250 try:
251 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500252 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400253 p = p[key]
254 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400255 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400256 return None
257
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400258 if self._developerKey:
259 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100260 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400261 q.append(('key', self._developerKey))
262 parsed[4] = urllib.urlencode(q)
263 url = urlparse.urlunparse(parsed)
264
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400265 headers = {}
266 headers, params, query, body = self._model.request(headers, {}, {}, None)
267
268 logging.info('URL being requested: %s' % url)
269 resp, content = self._http.request(url, method='GET', headers=headers)
270
Joe Gregorioaf276d22010-12-09 14:26:58 -0500271 return self._requestBuilder(self._http, url, method='GET',
272 headers=headers,
273 postproc=self._model.response,
274 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400275
276 setattr(theclass, methodName, method)
277
278 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400279 if 'methods' in resourceDesc:
280 for methodName, methodDesc in resourceDesc['methods'].iteritems():
281 if futureDesc:
282 future = futureDesc['methods'].get(methodName, {})
283 else:
284 future = None
285 createMethod(Resource, methodName, methodDesc, future)
286
287 # Add in nested resources
288 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500289
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400290 def createMethod(theclass, methodName, methodDesc, futureDesc):
291
292 def method(self):
293 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio3fada332011-01-07 17:07:45 -0500294 self._requestBuilder, methodName,
295 self._developerKey, methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400296
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()