1# coding=utf-8 2# 3# Copyright © 2013 Hewlett-Packard Development Company, L.P. 4# 5# This work is distributed under the W3C® Software License [1] 6# in the hope that it will be useful, but WITHOUT ANY 7# WARRANTY; without even the implied warranty of 8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 9# 10# [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 11# 12 13# Process URI templates per http://tools.ietf.org/html/rfc6570 14 15 16import urllib2 17import urlparse 18import json 19import base64 20import contextlib 21import collections 22import UserString 23 24import uritemplate 25 26class MimeType(UserString.MutableString): 27 def __init__(self, mimeType): 28 UserString.MutableString.__init__(self, mimeType) 29 self._type = None 30 self._subtype = None 31 self._structure = None 32 33 slashIndex = mimeType.find('/') 34 if (-1 < slashIndex): 35 self._type = mimeType[:slashIndex] 36 mimeType = mimeType[slashIndex + 1:] 37 plusIndex = mimeType.find('+') 38 if (-1 < plusIndex): 39 self._subtype = mimeType[:plusIndex] 40 self._structure = mimeType[plusIndex + 1:] 41 else: 42 self._structure = mimeType 43 else: 44 self._type = mimeType 45 46 def _update(self): 47 if (self._structure): 48 if (self._subtype): 49 self.data = self._type + '/' + self._subtype + '+' + self._structure 50 else: 51 self.data = self._type + '/' + self._structure 52 else: 53 self.data = self._type 54 55 def set(self, type, structure, subtype = None): 56 self._type = type 57 self._subtype = subtype 58 self._structure = structure 59 self._update() 60 61 @property 62 def type(self): 63 return self._type 64 65 @type.setter 66 def type(self, value): 67 self._type = value 68 self._update() 69 70 @property 71 def subtype(self): 72 return self._subtype 73 74 @subtype.setter 75 def subtype(self, value): 76 self._subtype = value 77 self._update() 78 79 @property 80 def structure(self): 81 return self._structure 82 83 @structure.setter 84 def structure(self, value): 85 self._structure = value 86 self._update() 87 88 89class APIResponse(object): 90 def __init__(self, response): 91 self.status = response.getcode() if (response) else 0 92 self.headers = response.info() if (response) else {} 93 self.data = response.read() if (200 == self.status) else None 94 95 if (self.data and 96 (('json' == self.contentType.structure) or ('json-home' == self.contentType.structure))): 97 try: 98 self.data = json.loads(self.data, object_pairs_hook = collections.OrderedDict) 99 except: 100 pass 101 102 @property 103 def contentType(self): 104 contentType = self.headers.get('content-type') if (self.headers) else None 105 return MimeType(contentType.split(';')[0]) if (contentType and (';' in contentType)) else MimeType(contentType) 106 107 @property 108 def encoding(self): 109 contentType = self.headers.get('content-type') if (self.headers) else None 110 if (contentType and (';' in contentType)): 111 encoding = contentType.split(';', 1)[1] 112 if ('=' in encoding): 113 return encoding.split('=', 1)[1].strip() 114 return 'utf-8' 115 116 117class APIHints(object): 118 def __init__(self, data): 119 self.httpMethods = [method.upper() for method in data['allow'] if method] if ('allow' in data) else ['GET'] 120 self.formats = {} 121 formats = [MimeType(format) for format in data['formats']] if ('formats' in data) else [] 122 if (formats): 123 if ('GET' in self.httpMethods): 124 self.formats['GET'] = formats 125 if ('PUT' in self.httpMethods): 126 self.formats['PUT'] = formats 127 128 if (('PATCH' in self.httpMethods) and ('accept-patch' in data)): 129 self.formats['PATCH'] = [MimeType(format) for format in data['accept-patch']] 130 if (('POST' in self.httpMethods) and ('accept-post' in data)): 131 self.formats['POST'] = [MimeType(format) for format in data['accept-post']] 132 133 # TODO: ranges from 'accept-ranges'; preferece tokens from 'accept-prefer'; 134 # preconditions from 'precondition-req'; auth from 'auth-req' 135 self.ranges = None 136 self.preferences = None 137 self.preconditions = None 138 self.auth = None 139 140 self.docs = data.get('docs') 141 self.status = data.get('status') 142 143 144class APIResource(object): 145 def __init__(self, baseURI, uri, variables = None, hints = None): 146 try: 147 self.template = uritemplate.URITemplate(urlparse.urljoin(baseURI, uri)) 148 if (variables): 149 self.variables = {variable: urlparse.urljoin(baseURI, variables[variable]) for variable in variables} 150 else: 151 self.variables = {variable: '' for variable in self.template.variables} 152 self.hints = hints 153 except Exception as e: 154 self.template = uritemplate.URITemplate('') 155 self.variables = {} 156 self.hints = None 157 158 159class APIClient(object): 160 def __init__(self, baseURI, version = None, username = None, password = None): 161 self._baseURI = baseURI 162 self.defaultVersion = version 163 self.defaultAccept = 'application/json' 164 self.username = username 165 self.password = password 166 self._resources = {} 167 self._versions = {} 168 self._accepts = {} 169 170 self._loadHome() 171 172 173 @property 174 def baseURI(self): 175 return self._baseURI 176 177 def _loadHome(self): 178 home = self._callURI('GET', self.baseURI, 'application/home+json, application/json-home, application/json') 179 if (home): 180 if ('application/json' == home.contentType): 181 for name in home.data: 182 apiKey = urlparse.urljoin(self.baseURI, name) 183 self._resources[apiKey] = APIResource(self.baseURI, home.data[name]) 184 elif (('application/home+json' == home.contentType) or 185 ('application/json-home' == home.contentType)): 186 resources = home.data.get('resources') 187 if (resources): 188 for name in resources: 189 apiKey = urlparse.urljoin(self.baseURI, name) 190 data = resources[name] 191 uri = data['href'] if ('href' in data) else data.get('href-template') 192 variables = data.get('href-vars') 193 hints = APIHints(data['hints']) if ('hints' in data) else None 194 self._resources[apiKey] = APIResource(self.baseURI, uri, variables, hints) 195 196 197 def relativeURI(self, uri): 198 if (uri.startswith(self.baseURI)): 199 relative = uri[len(self.baseURI):] 200 if (relative.startswith('/') and not self.baseURI.endswith('/')): 201 relative = relative[1:] 202 return relative 203 return uri 204 205 @property 206 def resourceNames(self): 207 return [self.relativeURI(apiKey) for apiKey in self._resources] 208 209 def resource(self, name): 210 return self._resources.get(urlparse.urljoin(self.baseURI, name)) 211 212 def addResource(self, name, uri): 213 resource = APIResource(self.baseURI, uri) 214 apiKey = urlparse.urljoin(self.baseURI, name) 215 self._resources[apiKey] = resource 216 217 def _accept(self, resource): 218 version = None 219 if (api and (api in self._versions)): 220 version = self._versions[api] 221 if (not version): 222 version = self.defaultVersion 223 return ('application/' + version + '+json, application/json') if (version) else 'application/json' 224 225 def _callURI(self, method, uri, accept, payload = None, payloadType = None): 226 try: 227 request = urllib2.Request(uri, data = payload, headers = { 'Accept' : accept }) 228 if (self.username and self.password): 229 request.add_header('Authorization', b'Basic ' + base64.b64encode(self.username + b':' + self.password)) 230 if (payload and payloadType): 231 request.add_header('Content-Type', payloadType) 232 request.get_method = lambda: method 233 234 with contextlib.closing(urllib2.urlopen(request)) as response: 235 return APIResponse(response) 236 except Exception as e: 237 pass 238 return None 239 240 def _call(self, method, name, arguments, payload = None, payloadType = None): 241 apiKey = urlparse.urljoin(self.baseURI, name) 242 resource = self._resources.get(apiKey) 243 244 if (resource): 245 uri = resource.template.expand(**arguments) 246 if (uri): 247 version = self._versions.get(apiKey) if (apiKey in self._versions) else self.defaultVersion 248 accept = MimeType(self._accepts(apiKey) if (apiKey in self._accepts) else self.defaultAccept) 249 if (version): 250 accept.subtype = version 251 return self._callURI(method, uri, accept, payload, payloadType) 252 return None 253 254 def setVersion(self, name, version): 255 apiKey = urlparse.urljoin(self.baseURI, name) 256 self._versions[apiKey] = version 257 258 def setAccept(self, name, mimeType): 259 apiKey = urlparse.urljoin(self.baseURI, name) 260 self._accepts[apiKey] = mimeType 261 262 def get(self, name, **kwargs): 263 return self._call('GET', name, kwargs) 264 265 def post(self, name, payload = None, payloadType = None, **kwargs): 266 return self._call('POST', name, kwargs, payload, payloadType) 267 268 def postForm(self, name, payload = None, **kwargs): 269 return self._call('POST', name, kwargs, urllib.urlencode(payload), 'application/x-www-form-urlencoded') 270 271 def postJSON(self, name, payload = None, **kwargs): 272 return self._call('POST', name, kwargs, json.dumps(payload), 'application/json') 273 274 def put(self, name, payload = None, payloadType = None, **kwargs): 275 return self._call('PUT', name, kwargs, payload, payloadType) 276 277 def patch(self, name, patch = None, **kwargs): 278 return self._call('PATCH', name, kwargs, json.dumps(patch), 'application/json-patch') 279 280 def delete(self, name, **kwargs): 281 return self._call('DELETE', name, kwargs) 282 283 284