Merge lp:~elachuni/piston-mini-client/failhandler into lp:piston-mini-client
- failhandler
- Merge into trunk
Proposed by
Anthony Lenton
Status: | Merged |
---|---|
Approved by: | Anthony Lenton |
Approved revision: | 32 |
Merged at revision: | 24 |
Proposed branch: | lp:~elachuni/piston-mini-client/failhandler |
Merge into: | lp:piston-mini-client |
Diff against target: |
699 lines (+451/-31) 12 files modified
doc/conf.py (+1/-1) doc/quickstart.rst (+47/-1) piston_mini_client/__init__.py (+18/-15) piston_mini_client/auth.py (+1/-1) piston_mini_client/failhandlers.py (+102/-0) piston_mini_client/serializers.py (+1/-1) piston_mini_client/test/test_auth.py (+4/-0) piston_mini_client/test/test_failhandlers.py (+254/-0) piston_mini_client/test/test_resource.py (+14/-10) piston_mini_client/test/test_serializers.py (+4/-0) piston_mini_client/test/test_validators.py (+4/-1) piston_mini_client/validators.py (+1/-1) |
To merge this branch: | bzr merge lp:~elachuni/piston-mini-client/failhandler |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Łukasz Czyżykowski (community) | Approve | ||
Łukasz Czyżykowski | Pending | ||
software-store-developers | Pending | ||
Review via email: mp+46001@code.launchpad.net |
Commit message
Description of the change
Overview
========
This branch adds fail handlers, to decouple the behaviour we provide when something goes wrong with the web service.
Details
=======
PistonAPI now uses a FailHandler class to process the response headers and body, before carrying on with deserialization. The FailHandler is instantiated with all the data from the request (so that it can do custom fail handling depending on the particular request, though none of the provided fail handlers does that atm), and must provide a single method handle(response, body) that can raise an exception, modify the body or return it as is.
Tests and docs included.
To post a comment you must log in.
- 29. By Anthony Lenton
-
Couple of small typos in the docs.
- 30. By Anthony Lenton
-
Removed duplicate line from tests.
- 31. By Anthony Lenton
-
Updated copyright year.
- 32. By Anthony Lenton
-
Factored out a bit of repeated code.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doc/conf.py' |
2 | --- doc/conf.py 2010-12-09 21:02:11 +0000 |
3 | +++ doc/conf.py 2011-01-18 14:47:27 +0000 |
4 | @@ -41,7 +41,7 @@ |
5 | |
6 | # General information about the project. |
7 | project = u'piston_mini_client' |
8 | -copyright = u'2010, Anthony Lenton' |
9 | +copyright = u'2010-2011, Canonical Ltd.' |
10 | |
11 | # The version info for the project you're documenting, acts as replacement for |
12 | # |version| and |release|, also used in various other places throughout the |
13 | |
14 | === modified file 'doc/quickstart.rst' |
15 | --- doc/quickstart.rst 2010-12-09 21:02:11 +0000 |
16 | +++ doc/quickstart.rst 2011-01-18 14:47:27 +0000 |
17 | @@ -5,6 +5,7 @@ |
18 | your api provides. Each method can specify what arguments it takes, what |
19 | authentication method should be used, and how to process the response. |
20 | |
21 | +================= |
22 | One simple method |
23 | ================= |
24 | |
25 | @@ -41,10 +42,12 @@ |
26 | level http calls via ``httplib2``. |
27 | |
28 | |
29 | +====================================== |
30 | Validating arguments to your API calls |
31 | ====================================== |
32 | |
33 | -If your ``urls.py`` specifies placeholders for resource arguments, as in:: |
34 | +If your server's ``urls.py`` specifies placeholders for resource arguments, as |
35 | +in:: |
36 | |
37 | urlpatterns = patterns('', |
38 | url(r'^foo/(?P<language>[^/]+)/(?P<foobar_id>\d+)/frobble$', |
39 | @@ -92,6 +95,7 @@ |
40 | |
41 | Then again, if we use this we'd need to then ensure that ``foobar_id >= 0``. |
42 | |
43 | +============================================== |
44 | Getting back light-weight objects from the API |
45 | ============================================== |
46 | |
47 | @@ -140,3 +144,45 @@ |
48 | and specified ``@returns(PistonResponse)`` but it might be nice to be able to |
49 | print one of these responses and get a meaningful output, or we might want |
50 | to attach some other method to ``FooBarResponse``. |
51 | + |
52 | +================ |
53 | +Handling Failure |
54 | +================ |
55 | + |
56 | +A common issue is what to do if the webservice returns an error. One possible |
57 | +solution (and the default for ``piston-mini-client``) is to raise an |
58 | +exception. |
59 | + |
60 | +This might not be the best solution for everybody, so piston-mini-client |
61 | +allows you to customize the way in which such failures are handled. |
62 | + |
63 | +You can do this by defining a ``FailHandler`` class. This class needs to |
64 | +provide a single method (``handle``), that receives the response headers and |
65 | +body, and decides what to do with it all. It can raise an exception, or |
66 | +modify the body in any way. |
67 | + |
68 | +If it returns a string this will be assumed to be the (possibly |
69 | +fixed) body of the response and will be deserialized by any decorators the |
70 | +method has. |
71 | + |
72 | +To use a different fail handler in your ``PistonAPI`` set the ``fail_handler`` |
73 | +class attribute . For example, to use the |
74 | +``NoneFailHandler`` instead of the default ``ExceptionFailHandler``, |
75 | +you can use:: |
76 | + |
77 | + class MyAPI(PistonAPI): |
78 | + fail_handler = NoneFailHandler |
79 | + # ... rest of the client definition... |
80 | + |
81 | +``piston-mini-client`` provides four fail handlers out of the box: |
82 | + |
83 | + * ``ExceptionFailHandler``: The default fail handler, raises ``APIError`` if |
84 | + anything goes wrong |
85 | + * ``NoneFailHandler``: Returns None if anything goes wrong. This will |
86 | + provide no information about *what* went wrong, so only use it if you don't |
87 | + really care. |
88 | + * ``DictFailHandler``: If anything goes wrong it returns a dict with all the |
89 | + headers and body for you to debug. |
90 | + * ``MultiExceptionFailHandler``: Raises a different exception according to |
91 | + what went wrong. |
92 | + |
93 | |
94 | === modified file 'piston_mini_client/__init__.py' |
95 | --- piston_mini_client/__init__.py 2010-12-22 20:49:37 +0000 |
96 | +++ piston_mini_client/__init__.py 2011-01-18 14:47:27 +0000 |
97 | @@ -1,5 +1,5 @@ |
98 | # -*- coding: utf-8 -*- |
99 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
100 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
101 | # GNU Lesser General Public License version 3 (see the file LICENSE). |
102 | |
103 | import httplib2 |
104 | @@ -7,17 +7,14 @@ |
105 | import urllib |
106 | from functools import wraps |
107 | |
108 | -class APIError(Exception): |
109 | - def __init__(self, msg, body=None): |
110 | - self.msg = msg |
111 | - self.body = body |
112 | - def __str__(self): |
113 | - return self.msg |
114 | +from failhandlers import ExceptionFailHandler, APIError |
115 | |
116 | def returns_json(func): |
117 | @wraps(func) |
118 | def wrapper(*args, **kwargs): |
119 | - response, body = func(*args, **kwargs) |
120 | + body = func(*args, **kwargs) |
121 | + if not isinstance(body, basestring): |
122 | + return body |
123 | return simplejson.loads(body) |
124 | return wrapper |
125 | |
126 | @@ -35,8 +32,10 @@ |
127 | def decorator(func): |
128 | @wraps(func) |
129 | def wrapper(self, *args, **kwargs): |
130 | - response, body = func(self, *args, **kwargs) |
131 | - return cls.from_response(response, body, none_allowed) |
132 | + body = func(self, *args, **kwargs) |
133 | + if not isinstance(body, basestring): |
134 | + return body |
135 | + return cls.from_response(body, none_allowed) |
136 | return wrapper |
137 | return decorator |
138 | |
139 | @@ -49,7 +48,9 @@ |
140 | def decorator(func): |
141 | @wraps(func) |
142 | def wrapper(self, *args, **kwargs): |
143 | - response, body = func(self, *args, **kwargs) |
144 | + body = func(self, *args, **kwargs) |
145 | + if not isinstance(body, basestring): |
146 | + return body |
147 | data = simplejson.loads(body) |
148 | items = [] |
149 | for datum in data: |
150 | @@ -63,7 +64,7 @@ |
151 | """Base class for objects that are returned from api calls. |
152 | """ |
153 | @classmethod |
154 | - def from_response(cls, response, body, none_allowed=False): |
155 | + def from_response(cls, body, none_allowed=False): |
156 | data = simplejson.loads(body) |
157 | if none_allowed and data is None: |
158 | return data |
159 | @@ -113,6 +114,8 @@ |
160 | |
161 | default_content_type = 'application/json' |
162 | |
163 | + fail_handler = ExceptionFailHandler |
164 | + |
165 | def __init__(self, service_root=None, cachedir=None, auth=None): |
166 | if service_root is None: |
167 | service_root = self.default_service_root |
168 | @@ -182,9 +185,9 @@ |
169 | raise APIError('Unable to connect to %s' % self._service_root) |
170 | else: |
171 | raise |
172 | - if response['status'] not in ['200', '201', '304']: |
173 | - raise APIError('%s: %s' % (response['status'],response), body) |
174 | - return response, body |
175 | + handler = self.fail_handler(url, method, body, headers) |
176 | + body = handler.handle(response, body) |
177 | + return body |
178 | |
179 | def _path2url(self, path): |
180 | return self._service_root.rstrip('/') + '/' + path.lstrip('/') |
181 | |
182 | === modified file 'piston_mini_client/auth.py' |
183 | --- piston_mini_client/auth.py 2010-12-09 17:06:20 +0000 |
184 | +++ piston_mini_client/auth.py 2011-01-18 14:47:27 +0000 |
185 | @@ -1,5 +1,5 @@ |
186 | # -*- coding: utf-8 -*- |
187 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
188 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
189 | # GNU Lesser General Public License version 3 (see the file LICENSE). |
190 | |
191 | from functools import wraps |
192 | |
193 | === added file 'piston_mini_client/failhandlers.py' |
194 | --- piston_mini_client/failhandlers.py 1970-01-01 00:00:00 +0000 |
195 | +++ piston_mini_client/failhandlers.py 2011-01-18 14:47:27 +0000 |
196 | @@ -0,0 +1,102 @@ |
197 | +# -*- coding: utf-8 -*- |
198 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
199 | +# GNU Lesser General Public License version 3 (see the file LICENSE). |
200 | + |
201 | +"""A fail handler is passed the raw httplib2 response and body, and has a |
202 | +chance to raise an exception, modify the body or return it unaltered, or |
203 | +even return a completely different object. It's up to the client (and |
204 | +possibly decorators) to know what to do with these returned objects. |
205 | +""" |
206 | + |
207 | +class APIError(Exception): |
208 | + def __init__(self, msg, body=None): |
209 | + self.msg = msg |
210 | + self.body = body |
211 | + def __str__(self): |
212 | + return self.msg |
213 | + |
214 | + |
215 | +class BaseFailHandler(object): |
216 | + """A base class for fail handlers. |
217 | + |
218 | + Child classes should at least define handle() |
219 | + """ |
220 | + success_status_codes = ['200', '201', '304'] |
221 | + |
222 | + def __init__(self, url, method, body, headers): |
223 | + """Don't store any of the provided information as we don't need it""" |
224 | + pass |
225 | + |
226 | + def handle(self, response, body): |
227 | + raise NotImplementedError() |
228 | + |
229 | + def was_error(self, response): |
230 | + """Returns True if 'response' is a failure""" |
231 | + return response.get('status') not in self.success_status_codes |
232 | + |
233 | +class ExceptionFailHandler(BaseFailHandler): |
234 | + """A fail handler that will raise APIErrors if anything goes wrong""" |
235 | + |
236 | + def handle(self, response, body): |
237 | + """Raise APIError if a strange status code is found""" |
238 | + if 'status' not in response: |
239 | + raise APIError('No status code in response') |
240 | + if self.was_error(response): |
241 | + raise APIError('%s: %s' % (response['status'], response), body) |
242 | + return body |
243 | + |
244 | + |
245 | +class NoneFailHandler(BaseFailHandler): |
246 | + """A fail handler that returns None if anything goes wrong. |
247 | + |
248 | + You probably only want to use this if you really don't care about what |
249 | + went wrong. |
250 | + """ |
251 | + def handle(self, response, body): |
252 | + """Raise APIError if a strange status code is found""" |
253 | + if self.was_error(response): |
254 | + return None |
255 | + return body |
256 | + |
257 | + |
258 | +class DictFailHandler(BaseFailHandler): |
259 | + """A fail handler that returns error information in a dict""" |
260 | + |
261 | + def handle(self, response, body): |
262 | + if self.was_error(response): |
263 | + return {'response': response, 'body': body} |
264 | + return body |
265 | + |
266 | + |
267 | +class BadRequestError(APIError): |
268 | + """A 400 Bad Request response was received""" |
269 | + |
270 | + |
271 | +class UnauthorizedError(APIError): |
272 | + """A 401 Bad Request response was received""" |
273 | + |
274 | + |
275 | +class NotFoundError(APIError): |
276 | + """A 404 Not Found response was received""" |
277 | + |
278 | + |
279 | +class InternalServerErrorError(APIError): |
280 | + """A 500 Internal Server Error response was received""" |
281 | + |
282 | + |
283 | +class MultiExceptionFailHandler(BaseFailHandler): |
284 | + """A fail handler that raises an exception according to what goes wrong""" |
285 | + exceptions = { |
286 | + '400': BadRequestError, |
287 | + '401': UnauthorizedError, |
288 | + '404': NotFoundError, |
289 | + '500': InternalServerErrorError, |
290 | + } |
291 | + |
292 | + def handle(self, response, body): |
293 | + if self.was_error(response): |
294 | + status = response.get('status') |
295 | + exception_class = self.exceptions.get(status, APIError) |
296 | + raise exception_class('%s: %s' % (status, response), body) |
297 | + return body |
298 | + |
299 | |
300 | === modified file 'piston_mini_client/serializers.py' |
301 | --- piston_mini_client/serializers.py 2010-12-09 21:02:11 +0000 |
302 | +++ piston_mini_client/serializers.py 2011-01-18 14:47:27 +0000 |
303 | @@ -1,5 +1,5 @@ |
304 | # -*- coding: utf-8 -*- |
305 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
306 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
307 | # GNU Lesser General Public License version 3 (see the file LICENSE). |
308 | |
309 | import simplejson |
310 | |
311 | === modified file 'piston_mini_client/test/test_auth.py' |
312 | --- piston_mini_client/test/test_auth.py 2010-12-09 21:02:11 +0000 |
313 | +++ piston_mini_client/test/test_auth.py 2011-01-18 14:47:27 +0000 |
314 | @@ -1,3 +1,7 @@ |
315 | +# -*- coding: utf-8 -*- |
316 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
317 | +# GNU Lesser General Public License version 3 (see the file LICENSE). |
318 | + |
319 | from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer |
320 | from unittest import TestCase |
321 | |
322 | |
323 | === added file 'piston_mini_client/test/test_failhandlers.py' |
324 | --- piston_mini_client/test/test_failhandlers.py 1970-01-01 00:00:00 +0000 |
325 | +++ piston_mini_client/test/test_failhandlers.py 2011-01-18 14:47:27 +0000 |
326 | @@ -0,0 +1,254 @@ |
327 | +# -*- coding: utf-8 -*- |
328 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
329 | +# GNU Lesser General Public License version 3 (see the file LICENSE). |
330 | + |
331 | +from mock import patch |
332 | +from unittest import TestCase |
333 | +from piston_mini_client import ( |
334 | + PistonAPI, |
335 | + PistonResponseObject, |
336 | + returns, |
337 | + returns_json, |
338 | + returns_list_of, |
339 | +) |
340 | +from piston_mini_client.failhandlers import ( |
341 | + APIError, |
342 | + BadRequestError, |
343 | + DictFailHandler, |
344 | + ExceptionFailHandler, |
345 | + InternalServerErrorError, |
346 | + MultiExceptionFailHandler, |
347 | + NoneFailHandler, |
348 | + NotFoundError, |
349 | + UnauthorizedError, |
350 | +) |
351 | + |
352 | +class GardeningAPI(PistonAPI): |
353 | + """Just a dummy API so we can play around with""" |
354 | + fail_handler = NoneFailHandler |
355 | + default_service_root = 'http://localhost:12345' |
356 | + |
357 | + @returns_json |
358 | + def grow(self): |
359 | + return self._post('/grow', {'plants': 'all'}) |
360 | + |
361 | + @returns(PistonResponseObject) |
362 | + def get_plant(self): |
363 | + return self._get('/plant') |
364 | + |
365 | + @returns_list_of(PistonResponseObject) |
366 | + def get_plants(self): |
367 | + return self._get('/plant') |
368 | + |
369 | + |
370 | +class ExceptionFailHandlerTestCase(TestCase): |
371 | + """As this is the default fail handler, we can skip most tests""" |
372 | + def test_no_status(self): |
373 | + """Check that an exception is raised if no status in response""" |
374 | + handler = ExceptionFailHandler('/foo', 'GET', '', {}) |
375 | + self.assertRaises(APIError, handler.handle, {}, '') |
376 | + |
377 | + def test_bad_status_codes(self): |
378 | + """Check that APIError is raised if bad status codes are returned""" |
379 | + bad_status = ['404', '500', '401'] |
380 | + handler = ExceptionFailHandler('/foo', 'GET', '', {}) |
381 | + for status in bad_status: |
382 | + self.assertRaises(APIError, handler.handle, |
383 | + {'status': status}, '') |
384 | + |
385 | + |
386 | +class NoneFailHandlerTestCase(TestCase): |
387 | + def test_no_status(self): |
388 | + handler = NoneFailHandler('/foo', 'GET', '', {}) |
389 | + self.assertEqual(None, handler.handle({}, 'not None')) |
390 | + |
391 | + def test_bad_status_codes(self): |
392 | + """Check that None is returned if bad status codes are returned""" |
393 | + bad_status = ['404', '500', '401'] |
394 | + handler = NoneFailHandler('/foo', 'GET', '', {}) |
395 | + for status in bad_status: |
396 | + self.assertEqual(None, handler.handle({'status': status}, '')) |
397 | + |
398 | + @patch('httplib2.Http.request') |
399 | + def test_interacts_well_with_returns_json_on_fail(self, mock_request): |
400 | + """Check that NoneFailHandler interacts well with returns_json""" |
401 | + mock_request.return_value = {'status': '500'}, 'invalid json' |
402 | + api = GardeningAPI() |
403 | + |
404 | + self.assertEqual(None, api.grow()) |
405 | + |
406 | + @patch('httplib2.Http.request') |
407 | + def test_interacts_well_with_returns_on_fail(self, mock_request): |
408 | + """Check that NoneFailHandler interacts well with returns""" |
409 | + mock_request.return_value = {'status': '500'}, 'invalid json' |
410 | + api = GardeningAPI() |
411 | + |
412 | + self.assertEqual(None, api.get_plant()) |
413 | + |
414 | + @patch('httplib2.Http.request') |
415 | + def test_interacts_well_with_returns_list_of_on_fail(self, mock_request): |
416 | + """Check that NoneFailHandler interacts well with returns_list_of""" |
417 | + mock_request.return_value = {'status': '500'}, 'invalid json' |
418 | + api = GardeningAPI() |
419 | + |
420 | + self.assertEqual(None, api.get_plants()) |
421 | + |
422 | + @patch('httplib2.Http.request') |
423 | + def test_interacts_well_with_returns_json(self, mock_request): |
424 | + """Check that NoneFailHandler interacts well with returns_json""" |
425 | + mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' |
426 | + api = GardeningAPI() |
427 | + |
428 | + self.assertEqual({'foo': 'bar'}, api.grow()) |
429 | + |
430 | + @patch('httplib2.Http.request') |
431 | + def test_interacts_well_with_returns(self, mock_request): |
432 | + """Check that NoneFailHandler interacts well with returns""" |
433 | + mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' |
434 | + api = GardeningAPI() |
435 | + |
436 | + self.assertTrue(isinstance(api.get_plant(), PistonResponseObject)) |
437 | + |
438 | + @patch('httplib2.Http.request') |
439 | + def test_interacts_well_with_returns_list_of(self, mock_request): |
440 | + """Check that NoneFailHandler interacts well with returns_list_of""" |
441 | + mock_request.return_value = {'status': '200'}, '[]' |
442 | + api = GardeningAPI() |
443 | + |
444 | + self.assertEqual([], api.get_plants()) |
445 | + |
446 | + |
447 | +class DictFailHandlerTestCase(TestCase): |
448 | + def setUp(self): |
449 | + self.response = {'status': '500'} |
450 | + self.body = 'invalid json' |
451 | + self.expected = {'response': self.response, 'body': self.body} |
452 | + self.api = GardeningAPI() |
453 | + self.api.fail_handler = DictFailHandler |
454 | + |
455 | + def test_no_status(self): |
456 | + handler = DictFailHandler('/foo', 'GET', '', {}) |
457 | + del self.response['status'] |
458 | + |
459 | + self.assertEqual(self.expected, handler.handle({}, self.body)) |
460 | + |
461 | + def test_bad_status_codes(self): |
462 | + bad_status = ['404', '500', '401'] |
463 | + handler = DictFailHandler('/foo', 'GET', '', {}) |
464 | + for status in bad_status: |
465 | + self.response['status'] = status |
466 | + self.assertEqual(self.expected, handler.handle(**self.expected)) |
467 | + |
468 | + @patch('httplib2.Http.request') |
469 | + def test_interacts_well_with_returns_json_on_fail(self, mock_request): |
470 | + """Check that DictFailHandler interacts well with returns_json""" |
471 | + mock_request.return_value = self.response, self.body |
472 | + |
473 | + self.assertEqual(self.expected, self.api.grow()) |
474 | + |
475 | + @patch('httplib2.Http.request') |
476 | + def test_interacts_well_with_returns_on_fail(self, mock_request): |
477 | + """Check that NoneFailHandler interacts well with returns""" |
478 | + mock_request.return_value = self.response, self.body |
479 | + |
480 | + self.assertEqual(self.expected, self.api.get_plant()) |
481 | + |
482 | + @patch('httplib2.Http.request') |
483 | + def test_interacts_well_with_returns_list_of_on_fail(self, mock_request): |
484 | + """Check that NoneFailHandler interacts well with returns_list_of""" |
485 | + mock_request.return_value = self.response, self.body |
486 | + |
487 | + self.assertEqual(self.expected, self.api.get_plants()) |
488 | + |
489 | + @patch('httplib2.Http.request') |
490 | + def test_interacts_well_with_returns_json(self, mock_request): |
491 | + """Check that NoneFailHandler interacts well with returns_json""" |
492 | + mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' |
493 | + |
494 | + self.assertEqual({'foo': 'bar'}, self.api.grow()) |
495 | + |
496 | + @patch('httplib2.Http.request') |
497 | + def test_interacts_well_with_returns(self, mock_request): |
498 | + """Check that NoneFailHandler interacts well with returns""" |
499 | + mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' |
500 | + |
501 | + self.assertTrue(isinstance(self.api.get_plant(), |
502 | + PistonResponseObject)) |
503 | + |
504 | + @patch('httplib2.Http.request') |
505 | + def test_interacts_well_with_returns_list_of(self, mock_request): |
506 | + """Check that NoneFailHandler interacts well with returns_list_of""" |
507 | + mock_request.return_value = {'status': '200'}, '[]' |
508 | + |
509 | + self.assertEqual([], self.api.get_plants()) |
510 | + |
511 | + |
512 | +class MultiExceptionFailHandlerTestCase(TestCase): |
513 | + def setUp(self): |
514 | + self.api = GardeningAPI() |
515 | + self.api.fail_handler = MultiExceptionFailHandler |
516 | + |
517 | + def test_no_status(self): |
518 | + handler = MultiExceptionFailHandler('/foo', 'GET', '', {}) |
519 | + |
520 | + self.assertRaises(APIError, handler.handle, {}, '') |
521 | + |
522 | + def test_bad_status_codes(self): |
523 | + bad_status = { |
524 | + '400': BadRequestError, |
525 | + '401': UnauthorizedError, |
526 | + '404': NotFoundError, |
527 | + '500': InternalServerErrorError, |
528 | + } |
529 | + handler = MultiExceptionFailHandler('/foo', 'GET', '', {}) |
530 | + for status, exception in bad_status.items(): |
531 | + self.assertRaises(exception, handler.handle, {'status': status}, |
532 | + '') |
533 | + |
534 | + @patch('httplib2.Http.request') |
535 | + def test_interacts_well_with_returns_json_on_fail(self, mock_request): |
536 | + """ Check that MultiExceptionFailHandler interacts well with |
537 | + returns_json""" |
538 | + mock_request.return_value = {'status': '401'}, '' |
539 | + |
540 | + self.assertRaises(UnauthorizedError, self.api.grow) |
541 | + |
542 | + @patch('httplib2.Http.request') |
543 | + def test_interacts_well_with_returns_on_fail(self, mock_request): |
544 | + """Check that MultiExceptionFailHandler interacts well with returns""" |
545 | + mock_request.return_value = {'status': '404'}, '' |
546 | + |
547 | + self.assertRaises(NotFoundError, self.api.get_plant) |
548 | + |
549 | + @patch('httplib2.Http.request') |
550 | + def test_interacts_well_with_returns_list_of_on_fail(self, mock_request): |
551 | + """ Check that MultiExceptionFailHandler interacts well with |
552 | + returns_list_of""" |
553 | + mock_request.return_value = {'status': '500'}, '' |
554 | + |
555 | + self.assertRaises(InternalServerErrorError, self.api.get_plants) |
556 | + |
557 | + @patch('httplib2.Http.request') |
558 | + def test_interacts_well_with_returns_json(self, mock_request): |
559 | + """ Check that MultiExceptionFailHandler interacts well with |
560 | + returns_json""" |
561 | + mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' |
562 | + |
563 | + self.assertEqual({'foo': 'bar'}, self.api.grow()) |
564 | + |
565 | + @patch('httplib2.Http.request') |
566 | + def test_interacts_well_with_returns(self, mock_request): |
567 | + """Check that MultiExceptionFailHandler interacts well with returns""" |
568 | + mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' |
569 | + |
570 | + self.assertTrue(isinstance(self.api.get_plant(), |
571 | + PistonResponseObject)) |
572 | + |
573 | + @patch('httplib2.Http.request') |
574 | + def test_interacts_well_with_returns_list_of(self, mock_request): |
575 | + """ Check that MultiExceptionFailHandler interacts well with |
576 | + returns_list_of""" |
577 | + mock_request.return_value = {'status': '200'}, '[]' |
578 | + |
579 | + self.assertEqual([], self.api.get_plants()) |
580 | + |
581 | |
582 | === modified file 'piston_mini_client/test/test_resource.py' |
583 | --- piston_mini_client/test/test_resource.py 2010-12-21 13:47:11 +0000 |
584 | +++ piston_mini_client/test/test_resource.py 2011-01-18 14:47:27 +0000 |
585 | @@ -1,3 +1,7 @@ |
586 | +# -*- coding: utf-8 -*- |
587 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
588 | +# GNU Lesser General Public License version 3 (see the file LICENSE). |
589 | + |
590 | from mock import patch |
591 | from unittest import TestCase |
592 | from wsgi_intercept import add_wsgi_intercept, remove_wsgi_intercept |
593 | @@ -79,12 +83,12 @@ |
594 | @patch('httplib2.Http.request') |
595 | def test_valid_status_codes_dont_raise_exception(self, mock_request): |
596 | for status in ['200', '201', '304']: |
597 | - expected_response = {'status': status} |
598 | + response = {'status': status} |
599 | expected_body = '"hello world!"' |
600 | - mock_request.return_value = (expected_response, expected_body) |
601 | + mock_request.return_value = (response, expected_body) |
602 | api = self.CoffeeAPI() |
603 | - response, body = api._get('/simmer') |
604 | - self.assertEqual(expected_response, response) |
605 | + body = api._get('/simmer') |
606 | + self.assertEqual(expected_body, body) |
607 | |
608 | def test_get_with_extra_args(self): |
609 | api = self.CoffeeAPI() |
610 | @@ -107,7 +111,7 @@ |
611 | |
612 | class PistonResponseObjectTestCase(TestCase): |
613 | def test_from_response(self): |
614 | - obj = PistonResponseObject.from_response({}, '{"foo": "bar"}') |
615 | + obj = PistonResponseObject.from_response('{"foo": "bar"}') |
616 | self.assertEqual('bar', obj.foo) |
617 | |
618 | def test_from_dict(self): |
619 | @@ -121,7 +125,7 @@ |
620 | default_service_root = 'foo' |
621 | @returns_json |
622 | def func(self): |
623 | - return {}, '{"foo": "bar", "baz": 42}' |
624 | + return '{"foo": "bar", "baz": 42}' |
625 | |
626 | result = MyAPI().func() |
627 | self.assertEqual({"foo": "bar", "baz": 42}, result) |
628 | @@ -133,7 +137,7 @@ |
629 | default_service_root = 'foo' |
630 | @returns(PistonResponseObject) |
631 | def func(self): |
632 | - return {}, '{"foo": "bar", "baz": 42}' |
633 | + return '{"foo": "bar", "baz": 42}' |
634 | |
635 | result = MyAPI().func() |
636 | self.assertTrue(isinstance(result, PistonResponseObject)) |
637 | @@ -143,7 +147,7 @@ |
638 | default_service_root = 'foo' |
639 | @returns(PistonResponseObject, none_allowed=True) |
640 | def func(self): |
641 | - return {}, 'null' |
642 | + return 'null' |
643 | |
644 | result = MyAPI().func() |
645 | self.assertEqual(result, None) |
646 | @@ -153,7 +157,7 @@ |
647 | default_service_root = 'foo' |
648 | @returns(PistonResponseObject, none_allowed=True) |
649 | def func(self): |
650 | - return {}, '{"foo": "bar", "baz": 42}' |
651 | + return '{"foo": "bar", "baz": 42}' |
652 | |
653 | result = MyAPI().func() |
654 | self.assertTrue(isinstance(result, PistonResponseObject)) |
655 | @@ -165,7 +169,7 @@ |
656 | default_service_root = 'foo' |
657 | @returns_list_of(PistonResponseObject) |
658 | def func(self): |
659 | - return {}, '[{"foo": "bar"}, {"baz": 42}]' |
660 | + return '[{"foo": "bar"}, {"baz": 42}]' |
661 | |
662 | result = MyAPI().func() |
663 | self.assertEqual(2, len(result)) |
664 | |
665 | === modified file 'piston_mini_client/test/test_serializers.py' |
666 | --- piston_mini_client/test/test_serializers.py 2010-12-09 21:02:11 +0000 |
667 | +++ piston_mini_client/test/test_serializers.py 2011-01-18 14:47:27 +0000 |
668 | @@ -1,3 +1,7 @@ |
669 | +# -*- coding: utf-8 -*- |
670 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
671 | +# GNU Lesser General Public License version 3 (see the file LICENSE). |
672 | + |
673 | from unittest import TestCase |
674 | |
675 | from piston_mini_client.serializers import JSONSerializer, FormSerializer |
676 | |
677 | === modified file 'piston_mini_client/test/test_validators.py' |
678 | --- piston_mini_client/test/test_validators.py 2010-12-09 21:02:11 +0000 |
679 | +++ piston_mini_client/test/test_validators.py 2011-01-18 14:47:27 +0000 |
680 | @@ -1,4 +1,7 @@ |
681 | -# nosetests --with-coverage --cover-html --cover-package=piston_mini_client |
682 | +# -*- coding: utf-8 -*- |
683 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
684 | +# GNU Lesser General Public License version 3 (see the file LICENSE). |
685 | + |
686 | from piston_mini_client.validators import (validate_pattern, validate, |
687 | validate_integer, oauth_protected, basic_protected, ValidationException) |
688 | from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer |
689 | |
690 | === modified file 'piston_mini_client/validators.py' |
691 | --- piston_mini_client/validators.py 2010-12-09 17:06:20 +0000 |
692 | +++ piston_mini_client/validators.py 2011-01-18 14:47:27 +0000 |
693 | @@ -1,5 +1,5 @@ |
694 | # -*- coding: utf-8 -*- |
695 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
696 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
697 | # GNU Lesser General Public License version 3 (see the file LICENSE). |
698 | |
699 | """ This module implements a bunch of decorators """ |
Looks good