blob: 3cd2c9fe1b9f5759510c8750246721fe4f885d9e [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 Gregorio48d361f2010-08-18 13:19:21 -040026import re
27import simplejson
28import urlparse
29import uritemplate
30
Joe Gregorio48d361f2010-08-18 13:19:21 -040031
Joe Gregorio41cf7972010-08-18 15:21:06 -040032class HttpError(Exception):
33 pass
34
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -040035DISCOVERY_URI = 'http://www.googleapis.com/discovery/0.1/describe\
36{?api,apiVersion}'
Joe Gregorio48d361f2010-08-18 13:19:21 -040037
38
39def key2method(key):
40 """
41 max-results -> MaxResults
42 """
43 result = []
44 key = list(key)
45 newWord = True
46 if not key[0].isalpha():
47 result.append('X')
48 newWord = False
49 for c in key:
50 if c.isalnum():
51 if newWord:
52 result.append(c.upper())
53 newWord = False
54 else:
55 result.append(c.lower())
56 else:
57 newWord = True
58
59 return ''.join(result)
60
61
62def key2param(key):
63 """
64 max-results -> max_results
65 """
66 result = []
67 key = list(key)
68 if not key[0].isalpha():
69 result.append('x')
70 for c in key:
71 if c.isalnum():
72 result.append(c)
73 else:
74 result.append('_')
75
76 return ''.join(result)
77
78
79class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040080
ade@google.com850cf552010-08-20 23:24:56 +010081 def request(self, headers, path_params, query_params, body_value):
82 query = self.build_query(query_params)
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040083 headers['accept'] = 'application/json'
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:
ade@google.com850cf552010-08-20 23:24:56 +010087 model = {'data': body_value}
Joe Gregorioba9ea7f2010-08-19 15:49:04 -040088 headers['content-type'] = 'application/json'
ade@google.com850cf552010-08-20 23:24:56 +010089 return (headers, path_params, query, simplejson.dumps(model))
90
91 def build_query(self, params):
92 query = '?alt=json&prettyprint=true'
93 for key,value in params.iteritems():
94 query += '&%s=%s' % (key, value)
95 return query
Joe Gregorio48d361f2010-08-18 13:19:21 -040096
97 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040098 # Error handling is TBD, for example, do we retry
99 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -0400100 if resp.status < 300:
101 return simplejson.loads(content)['data']
102 else:
ade@google.com850cf552010-08-20 23:24:56 +0100103 logging.debug('Content from bad request was: %s' % content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400104 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400105 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106 else:
107 raise HttpError(simplejson.loads(content)['error'])
108
109
110def build(service, version, http=httplib2.Http(),
Joe Gregorio41cf7972010-08-18 15:21:06 -0400111 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400112 params = {
113 'api': service,
114 'apiVersion': version
115 }
ade@google.com850cf552010-08-20 23:24:56 +0100116
117 requested_url = uritemplate.expand(discoveryServiceUrl, params)
118 logging.info('URL being requested: %s' % requested_url)
119 resp, content = http.request(requested_url)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400120 d = simplejson.loads(content)
121 service = d['data'][service][version]
122 base = service['baseUrl']
123 resources = service['resources']
124
125 class Service(object):
126 """Top level interface for a service"""
127
128 def __init__(self, http=http):
129 self._http = http
130 self._baseUrl = base
131 self._model = model
132
133 def createMethod(theclass, methodName, methodDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400134
Joe Gregorio48d361f2010-08-18 13:19:21 -0400135 def method(self, **kwargs):
136 return createResource(self._http, self._baseUrl, self._model,
137 methodName, methodDesc)
138
139 setattr(method, '__doc__', 'A description of how to use this function')
140 setattr(theclass, methodName, method)
141
142 for methodName, methodDesc in resources.iteritems():
143 createMethod(Service, methodName, methodDesc)
144 return Service()
145
146
147def createResource(http, baseUrl, model, resourceName, resourceDesc):
148
149 class Resource(object):
150 """A class for interacting with a resource."""
151
152 def __init__(self):
153 self._http = http
154 self._baseUrl = baseUrl
155 self._model = model
156
157 def createMethod(theclass, methodName, methodDesc):
158 pathUrl = methodDesc['pathUrl']
159 pathUrl = re.sub(r'\{', r'{+', pathUrl)
160 httpMethod = methodDesc['httpMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400161
ade@google.com850cf552010-08-20 23:24:56 +0100162 argmap = {}
163 if httpMethod in ['PUT', 'POST']:
164 argmap['body'] = 'body'
165
166
167 required_params = [] # Required parameters
168 pattern_params = {} # Parameters that must match a regex
169 query_params = [] # Parameters that will be used in the query string
170 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio21f11672010-08-18 17:23:17 -0400171 for arg, desc in methodDesc['parameters'].iteritems():
172 param = key2param(arg)
ade@google.com850cf552010-08-20 23:24:56 +0100173 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400174
ade@google.com850cf552010-08-20 23:24:56 +0100175 if desc.get('pattern', ''):
176 pattern_params[param] = desc['pattern']
177 if desc.get('required', False):
178 required_params.append(param)
179 if desc.get('parameterType') == 'query':
180 query_params.append(param)
181 if desc.get('parameterType') == 'path':
182 path_params[param]=param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400183
184 def method(self, **kwargs):
185 for name in kwargs.iterkeys():
186 if name not in argmap:
187 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400188
ade@google.com850cf552010-08-20 23:24:56 +0100189 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400190 if name not in kwargs:
191 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400192
ade@google.com850cf552010-08-20 23:24:56 +0100193 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400194 if name in kwargs:
195 if re.match(regex, kwargs[name]) is None:
Joe Gregorioba9ea7f2010-08-19 15:49:04 -0400196 raise TypeError('Parameter "%s" value "%s" does not match the pattern "%s"' % (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400197
ade@google.com850cf552010-08-20 23:24:56 +0100198 actual_query_params = {}
199 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400200 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100201 if key in query_params:
202 actual_query_params[argmap[key]] = value
203 if key in path_params:
204 actual_path_params[argmap[key]] = value
205 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400206
Joe Gregorio48d361f2010-08-18 13:19:21 -0400207 headers = {}
ade@google.com850cf552010-08-20 23:24:56 +0100208 headers, params, query, body = self._model.request(headers, actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400209
ade@google.com850cf552010-08-20 23:24:56 +0100210 expanded_url = uritemplate.expand(pathUrl, params)
211 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400212
ade@google.com850cf552010-08-20 23:24:56 +0100213 logging.info('URL being requested: %s' % url)
Joe Gregorio21f11672010-08-18 17:23:17 -0400214 resp, content = self._http.request(
215 url, method=httpMethod, headers=headers, body=body)
216
217 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400218
219 docs = ['A description of how to use this function\n\n']
220 for arg in argmap.iterkeys():
221 docs.append('%s - A parameter\n' % arg)
222
223 setattr(method, '__doc__', ''.join(docs))
224 setattr(theclass, methodName, method)
225
226 for methodName, methodDesc in resourceDesc['methods'].iteritems():
227 createMethod(Resource, methodName, methodDesc)
228
229 return Resource()