Merge lp:~canonical-isd-hackers/canonical-identity-provider/ssoclient into lp:~ubuntuone-pqm-team/canonical-identity-provider/ssoclient
- ssoclient
- Merge into ssoclient
Proposed by
Natalia Bidart
Status: | Merged |
---|---|
Approved by: | Natalia Bidart |
Approved revision: | no longer in the source branch. |
Merged at revision: | 17 |
Proposed branch: | lp:~canonical-isd-hackers/canonical-identity-provider/ssoclient |
Merge into: | lp:~ubuntuone-pqm-team/canonical-identity-provider/ssoclient |
Diff against target: |
374 lines (+136/-38) 8 files modified
Makefile (+5/-10) ssoclient/__init__.py (+1/-1) ssoclient/tests/__init__.py (+0/-1) ssoclient/tests/test_v2.py (+92/-18) ssoclient/v2/client.py (+23/-7) ssoclient/v2/errors.py (+3/-1) ssoclient/v2/http.py (+3/-0) tox.ini (+9/-0) |
To merge this branch: | bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/ssoclient |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Review via email: mp+241580@code.launchpad.net |
Commit message
Bump stable from trunk.
Description of the change
To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Makefile' | |||
2 | --- Makefile 2013-09-13 15:24:49 +0000 | |||
3 | +++ Makefile 2014-11-12 16:15:21 +0000 | |||
4 | @@ -1,19 +1,14 @@ | |||
9 | 1 | ENV_PATH=.env | 1 | TOX_PATH=.tox |
6 | 2 | PIP=pip | ||
7 | 3 | PYTHON=python | ||
8 | 4 | VIRTUALENV=virtualenv | ||
10 | 5 | 2 | ||
11 | 6 | .PHONY: clean env test | 3 | .PHONY: clean env test |
12 | 7 | 4 | ||
13 | 8 | all: clean env test | 5 | all: clean env test |
14 | 9 | 6 | ||
15 | 10 | env: | 7 | env: |
17 | 11 | $(VIRTUALENV) $(ENV_PATH) | 8 | tox --notest |
18 | 12 | 9 | ||
19 | 13 | clean: | 10 | clean: |
23 | 14 | rm -rf $(ENV_PATH) | 11 | rm -rf $(TOX_PATH) |
21 | 15 | rm -rf mock-*.egg oauthlib-*.egg requests-*.egg requests_oauthlib-*.egg | ||
22 | 16 | rm -rf ssoclient.egg-info | ||
24 | 17 | 12 | ||
27 | 18 | test: env | 13 | test: |
28 | 19 | . $(ENV_PATH)/bin/activate && $(PYTHON) setup.py test | 14 | tox |
29 | 20 | 15 | ||
30 | === modified file 'ssoclient/__init__.py' | |||
31 | --- ssoclient/__init__.py 2013-09-13 15:13:08 +0000 | |||
32 | +++ ssoclient/__init__.py 2014-11-12 16:15:21 +0000 | |||
33 | @@ -1,4 +1,4 @@ | |||
34 | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under |
35 | 2 | # the GNU Affero General Public License version 3 (see the file | 2 | # the GNU Affero General Public License version 3 (see the file |
36 | 3 | # LICENSE). | 3 | # LICENSE). |
38 | 4 | __version__ = '2.0' | 4 | __version__ = '2.1' |
39 | 5 | 5 | ||
40 | === modified file 'ssoclient/tests/__init__.py' | |||
41 | --- ssoclient/tests/__init__.py 2013-09-13 15:13:08 +0000 | |||
42 | +++ ssoclient/tests/__init__.py 2014-11-12 16:15:21 +0000 | |||
43 | @@ -1,4 +1,3 @@ | |||
44 | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under |
45 | 2 | # the GNU Affero General Public License version 3 (see the file | 2 | # the GNU Affero General Public License version 3 (see the file |
46 | 3 | # LICENSE). | 3 | # LICENSE). |
47 | 4 | |||
48 | 5 | 4 | ||
49 | === modified file 'ssoclient/tests/test_v2.py' | |||
50 | --- ssoclient/tests/test_v2.py 2013-11-07 18:27:41 +0000 | |||
51 | +++ ssoclient/tests/test_v2.py 2014-11-12 16:15:21 +0000 | |||
52 | @@ -3,12 +3,21 @@ | |||
53 | 3 | # Copyright 2013 Canonical Ltd. This software is licensed under | 3 | # Copyright 2013 Canonical Ltd. This software is licensed under |
54 | 4 | # the GNU Affero General Public License version 3 (see the file | 4 | # the GNU Affero General Public License version 3 (see the file |
55 | 5 | # LICENSE). | 5 | # LICENSE). |
56 | 6 | |||
57 | 7 | from __future__ import unicode_literals | ||
58 | 8 | |||
59 | 9 | try: | ||
60 | 10 | str = unicode | ||
61 | 11 | except NameError: | ||
62 | 12 | pass # Forward compatibility with Py3k | ||
63 | 13 | |||
64 | 6 | import json | 14 | import json |
65 | 7 | import unittest | 15 | import unittest |
66 | 8 | 16 | ||
67 | 9 | from datetime import datetime | 17 | from datetime import datetime |
68 | 10 | 18 | ||
69 | 11 | from mock import ( | 19 | from mock import ( |
70 | 20 | ANY, | ||
71 | 12 | MagicMock, | 21 | MagicMock, |
72 | 13 | patch, | 22 | patch, |
73 | 14 | ) | 23 | ) |
74 | @@ -92,7 +101,7 @@ | |||
75 | 92 | response = Response() | 101 | response = Response() |
76 | 93 | response.status_code = status_code | 102 | response.status_code = status_code |
77 | 94 | if content is not None and json_dump: | 103 | if content is not None and json_dump: |
79 | 95 | content = json.dumps(content) | 104 | content = str(json.dumps(content)).encode('utf-8') |
80 | 96 | response._content = content | 105 | response._content = content |
81 | 97 | return response | 106 | return response |
82 | 98 | 107 | ||
83 | @@ -105,7 +114,7 @@ | |||
84 | 105 | return response | 114 | return response |
85 | 106 | 115 | ||
86 | 107 | def test_error_code_raises_correct_exception(self): | 116 | def test_error_code_raises_correct_exception(self): |
88 | 108 | for code, exc in ERRORS.iteritems(): | 117 | for code, exc in ERRORS.items(): |
89 | 109 | response = mock_response(exc.status_code, dict(code=code)) | 118 | response = mock_response(exc.status_code, dict(code=code)) |
90 | 110 | with self.assertRaises(exc): | 119 | with self.assertRaises(exc): |
91 | 111 | self.do_test(response) | 120 | self.do_test(response) |
92 | @@ -167,6 +176,16 @@ | |||
93 | 167 | token_secret='token_secret', | 176 | token_secret='token_secret', |
94 | 168 | ) | 177 | ) |
95 | 169 | 178 | ||
96 | 179 | def assert_request_called_with(self, *args, **kwargs): | ||
97 | 180 | # grab parameters independently to ensure json-encoded data is correct | ||
98 | 181 | mock_args, mock_kwargs = self.mock_request.call_args | ||
99 | 182 | self.assertEqual(mock_args, args) | ||
100 | 183 | for k, v in kwargs.items(): | ||
101 | 184 | actual = mock_kwargs.get(k) | ||
102 | 185 | if k == 'data': | ||
103 | 186 | actual = json.loads(actual) | ||
104 | 187 | self.assertEqual(actual, v) | ||
105 | 188 | |||
106 | 170 | def unparsed_account_details(self, expand=False): | 189 | def unparsed_account_details(self, expand=False): |
107 | 171 | """Unparsed account details as how parse_response returns them.""" | 190 | """Unparsed account details as how parse_response returns them.""" |
108 | 172 | result = { | 191 | result = { |
109 | @@ -241,7 +260,7 @@ | |||
110 | 241 | credentials['consumer_key'], credentials['consumer_secret'], | 260 | credentials['consumer_key'], credentials['consumer_secret'], |
111 | 242 | credentials['token_key'], credentials['token_secret'], | 261 | credentials['token_key'], credentials['token_secret'], |
112 | 243 | ) | 262 | ) |
114 | 244 | self.assertTrue(all(isinstance(val, unicode) for | 263 | self.assertTrue(all(isinstance(val, str) for |
115 | 245 | val in self.mock_oauth.call_args[0])) | 264 | val in self.mock_oauth.call_args[0])) |
116 | 246 | 265 | ||
117 | 247 | 266 | ||
118 | @@ -307,6 +326,63 @@ | |||
119 | 307 | self.assert_invalid_response(500, errors.ServerError) | 326 | self.assert_invalid_response(500, errors.ServerError) |
120 | 308 | 327 | ||
121 | 309 | 328 | ||
122 | 329 | class GetOrCreateAccountTestCase(V2ClientApiTestCase): | ||
123 | 330 | |||
124 | 331 | def assert_invalid_response(self, status_code, ExceptionClass): | ||
125 | 332 | # Test the client can handle an error response that doesn't have | ||
126 | 333 | # a json body - ideally our server will never send these | ||
127 | 334 | response = mock_response( | ||
128 | 335 | status_code=status_code, content='some error message', | ||
129 | 336 | json_dump=False) | ||
130 | 337 | |||
131 | 338 | self.mock_request.return_value = response | ||
132 | 339 | with self.assertRaises(ExceptionClass) as ctx: | ||
133 | 340 | self.client.get_or_create_account( | ||
134 | 341 | token=self.credentials, email='blah') | ||
135 | 342 | |||
136 | 343 | if status_code >= 500: | ||
137 | 344 | self.assertIn('some error message', str(ctx.exception)) | ||
138 | 345 | |||
139 | 346 | def test_register_invalid_data(self): | ||
140 | 347 | self.mock_request.return_value = mock_response( | ||
141 | 348 | 400, dict(code="INVALID_DATA")) | ||
142 | 349 | with self.assertRaises(errors.InvalidData): | ||
143 | 350 | self.client.get_or_create_account( | ||
144 | 351 | token=self.credentials, email='blah') | ||
145 | 352 | self.assert_unicode_credentials(self.credentials) | ||
146 | 353 | |||
147 | 354 | def test_register_already_registered(self): | ||
148 | 355 | content = self.unparsed_account_details() | ||
149 | 356 | self.mock_request.return_value = mock_response( | ||
150 | 357 | 200, content=content) | ||
151 | 358 | response, created = self.client.get_or_create_account( | ||
152 | 359 | token=self.credentials, email='blah') | ||
153 | 360 | |||
154 | 361 | self.assertFalse(created) | ||
155 | 362 | self.assertEqual(response, content) | ||
156 | 363 | self.assert_unicode_credentials(self.credentials) | ||
157 | 364 | |||
158 | 365 | def test_register_success(self): | ||
159 | 366 | content = self.unparsed_account_details() | ||
160 | 367 | self.mock_request.return_value = mock_response( | ||
161 | 368 | status_code=201, content=content) | ||
162 | 369 | |||
163 | 370 | response, created = self.client.get_or_create_account( | ||
164 | 371 | token=self.credentials, email='blah') | ||
165 | 372 | |||
166 | 373 | self.assertTrue(created) | ||
167 | 374 | self.assertEqual(response, content) | ||
168 | 375 | self.assert_unicode_credentials(self.credentials) | ||
169 | 376 | |||
170 | 377 | def test_invalid_response_400(self): | ||
171 | 378 | self.assert_invalid_response(400, errors.ClientError) | ||
172 | 379 | self.assert_unicode_credentials(self.credentials) | ||
173 | 380 | |||
174 | 381 | def test_invalid_response_500(self): | ||
175 | 382 | self.assert_invalid_response(500, errors.ServerError) | ||
176 | 383 | self.assert_unicode_credentials(self.credentials) | ||
177 | 384 | |||
178 | 385 | |||
179 | 310 | class LoginV2ClientApiTestCase(V2ClientApiTestCase): | 386 | class LoginV2ClientApiTestCase(V2ClientApiTestCase): |
180 | 311 | 387 | ||
181 | 312 | def test_login_invalid_data(self): | 388 | def test_login_invalid_data(self): |
182 | @@ -385,11 +461,10 @@ | |||
183 | 385 | response = self.client.request_password_reset(self.email) | 461 | response = self.client.request_password_reset(self.email) |
184 | 386 | 462 | ||
185 | 387 | self.assertEqual(response, content) | 463 | self.assertEqual(response, content) |
187 | 388 | self.mock_request.assert_called_once_with( | 464 | self.assert_request_called_with( |
188 | 389 | 'POST', 'http://foo.com/tokens/password', | 465 | 'POST', 'http://foo.com/tokens/password', |
189 | 390 | headers={'Content-Type': 'application/json'}, | 466 | headers={'Content-Type': 'application/json'}, |
192 | 391 | data='{"token": null, "email": "%s"}' % self.email, | 467 | data={'token': None, 'email': self.email}) |
191 | 392 | ) | ||
193 | 393 | 468 | ||
194 | 394 | def test_request_password_reset_without_email(self): | 469 | def test_request_password_reset_without_email(self): |
195 | 395 | self.mock_request.return_value = mock_response( | 470 | self.mock_request.return_value = mock_response( |
196 | @@ -411,11 +486,10 @@ | |||
197 | 411 | self.email, 'token1234') | 486 | self.email, 'token1234') |
198 | 412 | 487 | ||
199 | 413 | self.assertEqual(response, content) | 488 | self.assertEqual(response, content) |
205 | 414 | self.assertEqual(self.mock_request.call_args, [ | 489 | self.assert_request_called_with( |
206 | 415 | ('POST', 'http://foo.com/tokens/password'), | 490 | 'POST', 'http://foo.com/tokens/password', |
207 | 416 | {'headers': {'Content-Type': 'application/json'}, | 491 | headers={'Content-Type': 'application/json'}, |
208 | 417 | 'data': '{"token": "token1234", "email": "%s"}' % self.email}, | 492 | data={'token': 'token1234', 'email': self.email}) |
204 | 418 | ]) | ||
209 | 419 | 493 | ||
210 | 420 | def test_request_password_reset_for_suspended_account(self): | 494 | def test_request_password_reset_for_suspended_account(self): |
211 | 421 | self.mock_request.return_value = mock_response( | 495 | self.mock_request.return_value = mock_response( |
212 | @@ -644,10 +718,9 @@ | |||
213 | 644 | def assert_validate_request_called(self, **kwargs): | 718 | def assert_validate_request_called(self, **kwargs): |
214 | 645 | data = dict(http_url=self.http_url, http_method='GET') | 719 | data = dict(http_url=self.http_url, http_method='GET') |
215 | 646 | data.update(kwargs) | 720 | data.update(kwargs) |
217 | 647 | self.mock_request.assert_called_once_with( | 721 | self.assert_request_called_with( |
218 | 648 | 'POST', TEST_ENDPOINT + 'requests/validate', | 722 | 'POST', TEST_ENDPOINT + 'requests/validate', |
221 | 649 | headers={'Content-Type': 'application/json'}, | 723 | headers={'Content-Type': 'application/json'}, data=data) |
220 | 650 | data=json.dumps(data)) | ||
222 | 651 | 724 | ||
223 | 652 | def test_valid_request(self): | 725 | def test_valid_request(self): |
224 | 653 | self.mock_request.return_value = mock_response( | 726 | self.mock_request.return_value = mock_response( |
225 | @@ -717,7 +790,7 @@ | |||
226 | 717 | self.mock_request.return_value = mock_response( | 790 | self.mock_request.return_value = mock_response( |
227 | 718 | 200, dict(is_valid=True)) | 791 | 200, dict(is_valid=True)) |
228 | 719 | 792 | ||
230 | 720 | http_url = u'http://localhost/~/test/doc/dåc-id' | 793 | http_url = 'http://localhost/~/test/doc/dåc-id' |
231 | 721 | result = self.client.validate_request( | 794 | result = self.client.validate_request( |
232 | 722 | http_url=http_url, http_method='GET', authorization='something') | 795 | http_url=http_url, http_method='GET', authorization='something') |
233 | 723 | 796 | ||
234 | @@ -729,16 +802,17 @@ | |||
235 | 729 | self.mock_request.return_value = mock_response( | 802 | self.mock_request.return_value = mock_response( |
236 | 730 | 200, dict(is_valid=True)) | 803 | 200, dict(is_valid=True)) |
237 | 731 | 804 | ||
239 | 732 | http_url = u'http://localhost/~/test/doc/dåc-id'.encode('utf-8') | 805 | http_url = 'http://localhost/~/test/doc/dåc-id' |
240 | 733 | result = self.client.validate_request( | 806 | result = self.client.validate_request( |
242 | 734 | http_url=http_url, http_method='GET', authorization='something') | 807 | http_url=http_url.encode('utf-8'), http_method='GET', |
243 | 808 | authorization='something') | ||
244 | 735 | 809 | ||
245 | 736 | self.assertEqual(result, {'is_valid': True}) | 810 | self.assertEqual(result, {'is_valid': True}) |
246 | 737 | self.assert_validate_request_called( | 811 | self.assert_validate_request_called( |
247 | 738 | http_url=http_url, authorization='something') | 812 | http_url=http_url, authorization='something') |
248 | 739 | 813 | ||
249 | 740 | def test_non_ascii_url_not_utf8_encoded(self): | 814 | def test_non_ascii_url_not_utf8_encoded(self): |
251 | 741 | http_url = u'http://localhost/~/test/doc/dåc-id'.encode('latin-1') | 815 | http_url = 'http://localhost/~/test/doc/dåc-id'.encode('latin-1') |
252 | 742 | 816 | ||
253 | 743 | with self.assertRaises(errors.ClientError) as ctx: | 817 | with self.assertRaises(errors.ClientError) as ctx: |
254 | 744 | self.client.validate_request( | 818 | self.client.validate_request( |
255 | 745 | 819 | ||
256 | === modified file 'ssoclient/v2/client.py' | |||
257 | --- ssoclient/v2/client.py 2013-11-07 18:15:38 +0000 | |||
258 | +++ ssoclient/v2/client.py 2014-11-12 16:15:21 +0000 | |||
259 | @@ -1,6 +1,14 @@ | |||
260 | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under |
261 | 2 | # the GNU Affero General Public License version 3 (see the file | 2 | # the GNU Affero General Public License version 3 (see the file |
262 | 3 | # LICENSE). | 3 | # LICENSE). |
263 | 4 | |||
264 | 5 | from __future__ import unicode_literals | ||
265 | 6 | |||
266 | 7 | try: | ||
267 | 8 | str = unicode | ||
268 | 9 | except NameError: | ||
269 | 10 | pass # Forward compatibility with Py3k | ||
270 | 11 | |||
271 | 4 | import logging | 12 | import logging |
272 | 5 | 13 | ||
273 | 6 | from datetime import datetime | 14 | from datetime import datetime |
274 | @@ -27,7 +35,7 @@ | |||
275 | 27 | """Recursively look for dates and try to parse them into datetimes.""" | 35 | """Recursively look for dates and try to parse them into datetimes.""" |
276 | 28 | assert isinstance(value, dict) | 36 | assert isinstance(value, dict) |
277 | 29 | result = value.copy() | 37 | result = value.copy() |
279 | 30 | for k, v in value.iteritems(): | 38 | for k, v in value.items(): |
280 | 31 | if isinstance(v, dict): | 39 | if isinstance(v, dict): |
281 | 32 | result[k] = parse_datetimes(v) | 40 | result[k] = parse_datetimes(v) |
282 | 33 | elif isinstance(v, list): | 41 | elif isinstance(v, list): |
283 | @@ -49,10 +57,10 @@ | |||
284 | 49 | # json library is in use. | 57 | # json library is in use. |
285 | 50 | # oauthlib requires them to be unicode - so we coerce to be sure. | 58 | # oauthlib requires them to be unicode - so we coerce to be sure. |
286 | 51 | if credentials is not None: | 59 | if credentials is not None: |
291 | 52 | consumer_key = unicode(credentials.get('consumer_key', '')) | 60 | consumer_key = str(credentials.get('consumer_key', '')) |
292 | 53 | consumer_secret = unicode(credentials.get('consumer_secret', '')) | 61 | consumer_secret = str(credentials.get('consumer_secret', '')) |
293 | 54 | token_key = unicode(credentials.get('token_key', '')) | 62 | token_key = str(credentials.get('token_key', '')) |
294 | 55 | token_secret = unicode(credentials.get('token_secret', '')) | 63 | token_secret = str(credentials.get('token_secret', '')) |
295 | 56 | oauth = OAuth1( | 64 | oauth = OAuth1( |
296 | 57 | consumer_key, | 65 | consumer_key, |
297 | 58 | consumer_secret, | 66 | consumer_secret, |
298 | @@ -82,8 +90,16 @@ | |||
299 | 82 | result = parse_datetimes(response.content) | 90 | result = parse_datetimes(response.content) |
300 | 83 | return result | 91 | return result |
301 | 84 | 92 | ||
302 | 93 | def get_or_create_account(self, token, **kwargs): | ||
303 | 94 | oauth = self._unicode_credentials(token) | ||
304 | 95 | response = self.session.post( | ||
305 | 96 | '/accounts', data=kwargs, auth=oauth) | ||
306 | 97 | result = parse_datetimes(response.content) | ||
307 | 98 | created = response.status_code == 201 | ||
308 | 99 | return result, created | ||
309 | 100 | |||
310 | 85 | def account_details(self, openid, token=None, expand=False): | 101 | def account_details(self, openid, token=None, expand=False): |
312 | 86 | openid = unicode(openid) | 102 | openid = str(openid) |
313 | 87 | oauth = self._unicode_credentials(token) | 103 | oauth = self._unicode_credentials(token) |
314 | 88 | url = '/accounts/%s?expand=%s' % (openid, str(expand).lower()) | 104 | url = '/accounts/%s?expand=%s' % (openid, str(expand).lower()) |
315 | 89 | 105 | ||
316 | @@ -143,7 +159,7 @@ | |||
317 | 143 | raise errors.ClientError(msg=msg) | 159 | raise errors.ClientError(msg=msg) |
318 | 144 | 160 | ||
319 | 145 | http_url = data.get('http_url', '') | 161 | http_url = data.get('http_url', '') |
321 | 146 | if not isinstance(http_url, unicode): | 162 | if not isinstance(http_url, str): |
322 | 147 | try: | 163 | try: |
323 | 148 | data['http_url'] = http_url.decode('utf-8') | 164 | data['http_url'] = http_url.decode('utf-8') |
324 | 149 | except UnicodeError: | 165 | except UnicodeError: |
325 | 150 | 166 | ||
326 | === modified file 'ssoclient/v2/errors.py' | |||
327 | --- ssoclient/v2/errors.py 2013-09-30 20:47:01 +0000 | |||
328 | +++ ssoclient/v2/errors.py 2014-11-12 16:15:21 +0000 | |||
329 | @@ -2,6 +2,8 @@ | |||
330 | 2 | # the GNU Affero General Public License version 3 (see the file | 2 | # the GNU Affero General Public License version 3 (see the file |
331 | 3 | # LICENSE). | 3 | # LICENSE). |
332 | 4 | 4 | ||
333 | 5 | from __future__ import unicode_literals | ||
334 | 6 | |||
335 | 5 | class UnexpectedApiError(Exception): | 7 | class UnexpectedApiError(Exception): |
336 | 6 | """An unexpected client error.""" | 8 | """An unexpected client error.""" |
337 | 7 | 9 | ||
338 | @@ -39,7 +41,7 @@ | |||
339 | 39 | # of a subclass - so still fetch it from the payload body | 41 | # of a subclass - so still fetch it from the payload body |
340 | 40 | code = body.get('code') | 42 | code = body.get('code') |
341 | 41 | msg = "%s: %s" % (response.status_code, code) | 43 | msg = "%s: %s" % (response.status_code, code) |
343 | 42 | extra = ', '.join('%s: %r' % i for i in self.extra.iteritems()) | 44 | extra = ', '.join('%s: %r' % i for i in self.extra.items()) |
344 | 43 | if extra: | 45 | if extra: |
345 | 44 | msg += ' (%s)' % extra | 46 | msg += ' (%s)' % extra |
346 | 45 | super(ApiException, self).__init__(msg) | 47 | super(ApiException, self).__init__(msg) |
347 | 46 | 48 | ||
348 | === modified file 'ssoclient/v2/http.py' | |||
349 | --- ssoclient/v2/http.py 2013-10-01 00:17:10 +0000 | |||
350 | +++ ssoclient/v2/http.py 2014-11-12 16:15:21 +0000 | |||
351 | @@ -1,6 +1,9 @@ | |||
352 | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under |
353 | 2 | # the GNU Affero General Public License version 3 (see the file | 2 | # the GNU Affero General Public License version 3 (see the file |
354 | 3 | # LICENSE). | 3 | # LICENSE). |
355 | 4 | |||
356 | 5 | from __future__ import unicode_literals | ||
357 | 6 | |||
358 | 4 | import functools | 7 | import functools |
359 | 5 | import json | 8 | import json |
360 | 6 | 9 | ||
361 | 7 | 10 | ||
362 | === added file 'tox.ini' | |||
363 | --- tox.ini 1970-01-01 00:00:00 +0000 | |||
364 | +++ tox.ini 2014-11-12 16:15:21 +0000 | |||
365 | @@ -0,0 +1,9 @@ | |||
366 | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under | ||
367 | 2 | # the GNU Affero General Public License version 3 (see the file | ||
368 | 3 | # LICENSE). | ||
369 | 4 | |||
370 | 5 | [tox] | ||
371 | 6 | envlist = py27, py34 | ||
372 | 7 | |||
373 | 8 | [testenv] | ||
374 | 9 | commands = {envpython} setup.py test {posargs} |