blob: 7d884af333464660f9ebc29ff51c21ee59daa916 [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
25import re
26import simplejson
27import urlparse
28import uritemplate
29
Joe Gregorio48d361f2010-08-18 13:19:21 -040030
Joe Gregorio41cf7972010-08-18 15:21:06 -040031class HttpError(Exception):
32 pass
33
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -040034DISCOVERY_URI = 'http://www.googleapis.com/discovery/0.1/describe\
35{?api,apiVersion}'
Joe Gregorio48d361f2010-08-18 13:19:21 -040036
37
38def key2method(key):
39 """
40 max-results -> MaxResults
41 """
42 result = []
43 key = list(key)
44 newWord = True
45 if not key[0].isalpha():
46 result.append('X')
47 newWord = False
48 for c in key:
49 if c.isalnum():
50 if newWord:
51 result.append(c.upper())
52 newWord = False
53 else:
54 result.append(c.lower())
55 else:
56 newWord = True
57
58 return ''.join(result)
59
60
61def key2param(key):
62 """
63 max-results -> max_results
64 """
65 result = []
66 key = list(key)
67 if not key[0].isalpha():
68 result.append('x')
69 for c in key:
70 if c.isalnum():
71 result.append(c)
72 else:
73 result.append('_')
74
75 return ''.join(result)
76
77
78class JsonModel(object):
Joe Gregorio41cf7972010-08-18 15:21:06 -040079
Joe Gregorio48d361f2010-08-18 13:19:21 -040080 def request(self, headers, params):
81 model = params.get('body', None)
82 query = '?alt=json&prettyprint=true'
83 headers['Accept'] = 'application/json'
84 if model == None:
85 return (headers, params, query, None)
86 else:
Joe Gregorio41cf7972010-08-18 15:21:06 -040087 model = {'data': model}
Joe Gregorio48d361f2010-08-18 13:19:21 -040088 headers['Content-Type'] = 'application/json'
89 del params['body']
90 return (headers, params, query, simplejson.dumps(model))
91
92 def response(self, resp, content):
Joe Gregorio41cf7972010-08-18 15:21:06 -040093 # Error handling is TBD, for example, do we retry
94 # for some operation/error combinations?
Joe Gregorio48d361f2010-08-18 13:19:21 -040095 if resp.status < 300:
96 return simplejson.loads(content)['data']
97 else:
98 if resp['content-type'] != 'application/json':
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -040099 raise HttpError('%d %s' % (resp.status, resp.reason))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400100 else:
101 raise HttpError(simplejson.loads(content)['error'])
102
103
104def build(service, version, http=httplib2.Http(),
Joe Gregorio41cf7972010-08-18 15:21:06 -0400105 discoveryServiceUrl=DISCOVERY_URI, auth=None, model=JsonModel()):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400106 params = {
107 'api': service,
108 'apiVersion': version
109 }
110 resp, content = http.request(uritemplate.expand(discoveryServiceUrl, params))
111 d = simplejson.loads(content)
112 service = d['data'][service][version]
113 base = service['baseUrl']
114 resources = service['resources']
115
116 class Service(object):
117 """Top level interface for a service"""
118
119 def __init__(self, http=http):
120 self._http = http
121 self._baseUrl = base
122 self._model = model
123
124 def createMethod(theclass, methodName, methodDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400125
Joe Gregorio48d361f2010-08-18 13:19:21 -0400126 def method(self, **kwargs):
127 return createResource(self._http, self._baseUrl, self._model,
128 methodName, methodDesc)
129
130 setattr(method, '__doc__', 'A description of how to use this function')
131 setattr(theclass, methodName, method)
132
133 for methodName, methodDesc in resources.iteritems():
134 createMethod(Service, methodName, methodDesc)
135 return Service()
136
137
138def createResource(http, baseUrl, model, resourceName, resourceDesc):
139
140 class Resource(object):
141 """A class for interacting with a resource."""
142
143 def __init__(self):
144 self._http = http
145 self._baseUrl = baseUrl
146 self._model = model
147
148 def createMethod(theclass, methodName, methodDesc):
149 pathUrl = methodDesc['pathUrl']
150 pathUrl = re.sub(r'\{', r'{+', pathUrl)
151 httpMethod = methodDesc['httpMethod']
152 args = methodDesc['parameters'].keys()
Joe Gregorio21f11672010-08-18 17:23:17 -0400153
154 required = [] # Required parameters
155 pattern = {} # Parameters the must match a regex
156 for arg, desc in methodDesc['parameters'].iteritems():
157 param = key2param(arg)
158 if desc.get('pattern', ''):
159 pattern[param] = desc['pattern']
160 if desc.get('required', False):
161 required.append(param)
162
Joe Gregorio48d361f2010-08-18 13:19:21 -0400163 if httpMethod in ['PUT', 'POST']:
164 args.append('body')
165 argmap = dict([(key2param(key), key) for key in args])
166
167 def method(self, **kwargs):
168 for name in kwargs.iterkeys():
169 if name not in argmap:
170 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400171
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400172 for name in required:
173 if name not in kwargs:
174 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400175
176 for name, regex in pattern.iteritems():
177 if name in kwargs:
178 if re.match(regex, kwargs[name]) is None:
179 raise TypeError('Parameter "%s" value "%s" does match the pattern "%s"' % (name, kwargs[name], regex))
180
181 params = {}
182 for key, value in kwargs.iteritems():
183 params[argmap[key]] = value
184
Joe Gregorio48d361f2010-08-18 13:19:21 -0400185 headers = {}
186 headers, params, query, body = self._model.request(headers, params)
187
188 url = urlparse.urljoin(self._baseUrl,
189 uritemplate.expand(pathUrl, params) + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400190
Joe Gregorio21f11672010-08-18 17:23:17 -0400191 resp, content = self._http.request(
192 url, method=httpMethod, headers=headers, body=body)
193
194 return self._model.response(resp, content)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400195
196 docs = ['A description of how to use this function\n\n']
197 for arg in argmap.iterkeys():
198 docs.append('%s - A parameter\n' % arg)
199
200 setattr(method, '__doc__', ''.join(docs))
201 setattr(theclass, methodName, method)
202
203 for methodName, methodDesc in resourceDesc['methods'].iteritems():
204 createMethod(Resource, methodName, methodDesc)
205
206 return Resource()