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