Skip to content
This repository was archived by the owner on Apr 12, 2018. It is now read-only.

Commit 1cb669a

Browse files
committed
Merge pull request #36 from botify-labs/fix/unknown-resource-handling
Fix/unknown resource handling
2 parents 729be7f + 174043b commit 1cb669a

File tree

10 files changed

+495
-174
lines changed

10 files changed

+495
-174
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ python:
66
install: "python setup.py install"
77

88
# command to run tests
9-
script: nosetests
9+
script: nosetests --with-doctest

swf/exceptions.py

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
#
66
# See the file LICENSE for copying permission.
77

8+
import logging
9+
import collections
10+
from functools import wraps, partial
11+
import re
12+
13+
import boto.swf.exceptions
14+
815

916
class SWFError(Exception):
1017
def __init__(self, message, raw_error='', *args, **kwargs):
@@ -98,3 +105,290 @@ class AlreadyExistsError(SWFError):
98105

99106
class InvalidKeywordArgumentError(SWFError):
100107
pass
108+
109+
110+
def ignore(*args, **kwargs):
111+
return
112+
113+
114+
REGEX_UNKNOWN_RESOURCE = re.compile(r'^[^ ]+\s+([^ :]+)')
115+
REGEX_NESTED_RESOURCE = re.compile(r'Unknown (?:type|execution):\s*([^=]+)=\[')
116+
117+
118+
def match_equals(regex, string, values):
119+
"""
120+
Extract a value from a string with a regex and compare it.
121+
122+
:param regex: to extract the value to check.
123+
:type regex: _sre.SRE_Pattern (compiled regex)
124+
125+
:param string: that contains the value to extract.
126+
:type string: str
127+
128+
:param values: to compare with.
129+
:type values: [str]
130+
131+
"""
132+
if string is None:
133+
return False
134+
135+
matched = regex.findall(string)
136+
if not matched:
137+
return False
138+
139+
if (isinstance(values, basestring) and
140+
not isinstance(values, collections.Sequence)):
141+
values = (values,)
142+
return matched[0] in values
143+
144+
145+
def is_swf_response_error(error):
146+
"""
147+
Return true if *error* is a :class:`SWFResponseError` exception.
148+
149+
:param error: is the exception to check.
150+
:type error: Exception.
151+
152+
"""
153+
return isinstance(error, boto.swf.exceptions.SWFResponseError)
154+
155+
156+
def is_unknown_resource_raised(error, *args, **kwargs):
157+
"""
158+
Handler that checks if *error* is an unknown resource fault.
159+
160+
:param error: is the exception to check.
161+
:type error: Exception
162+
163+
"""
164+
if not isinstance(error, boto.swf.exceptions.SWFResponseError):
165+
return False
166+
167+
return getattr(error, 'error_code', None) == 'UnknownResourceFault'
168+
169+
170+
def is_unknown(resource):
171+
"""
172+
Return a function that checks if *error* is an unknown *resource* fault.
173+
174+
"""
175+
@wraps(is_unknown)
176+
def wrapped(error, *args, **kwargs):
177+
"""
178+
:param error: is the exception to check.
179+
:type error: Exception
180+
181+
"""
182+
if not is_unknown_resource_raised(error, *args, **kwargs):
183+
return False
184+
if getattr(error, 'error_code', None) != 'UnknownResourceFault':
185+
raise ValueError('cannot extract resource from {}'.format(
186+
error))
187+
188+
message = error.body.get('message')
189+
if match_equals(REGEX_UNKNOWN_RESOURCE,
190+
message,
191+
('type', 'execution')):
192+
return match_equals(REGEX_NESTED_RESOURCE, message, resource)
193+
return match_equals(REGEX_UNKNOWN_RESOURCE, message, resource)
194+
195+
return wrapped
196+
197+
198+
def always(value):
199+
"""
200+
Always return *value* whatever arguments it got.
201+
202+
Examples
203+
--------
204+
205+
>>> f = always(1)
206+
>>> f('a', 'b')
207+
1
208+
209+
>>> f = always(lambda: True)
210+
>>> f('foo')
211+
True
212+
213+
"""
214+
import types
215+
216+
@wraps(always)
217+
def wrapped(*args, **kwargs):
218+
if isinstance(value, types.FunctionType):
219+
return value()
220+
return value
221+
return wrapped
222+
223+
224+
def extract_resource(error):
225+
if getattr(error, 'error_code', None) != 'UnknownResourceFault':
226+
raise ValueError('cannot extract resource from {}'.format(
227+
error))
228+
229+
message = error.body.get('message')
230+
resource = (REGEX_UNKNOWN_RESOURCE.findall(message) if
231+
message else None)
232+
return "Resource {} does not exist".format(
233+
resource[0] if resource else 'unknown')
234+
235+
236+
def raises(exception, when, extract=str):
237+
"""
238+
:param exception: to raise when the predicate is True.
239+
:type exception: Exception
240+
241+
:param when: predicate to apply.
242+
:type when: (error, *args, **kwargs) -> bool
243+
244+
Examples
245+
--------
246+
247+
Let's build a :class:`boto.swf.exceptions.SWFResponseError` for an unknown
248+
execution:
249+
250+
>>> status = 400
251+
>>> reason = 'Bad Request'
252+
>>> body_type = 'com.amazonaws.swf.base.model#UnknownResourceFault'
253+
>>> body_message = 'Unknown execution: blah'
254+
>>> body = {'__type': body_type, 'message': body_message}
255+
>>> error_code = 'UnknownResourceFault'
256+
>>> from boto.swf.exceptions import SWFResponseError
257+
>>> err = SWFResponseError(status, reason, body, error_code)
258+
>>> raises(DoesNotExistError,
259+
... when=is_unknown_resource_raised,
260+
... extract=extract_resource)(err)
261+
Traceback (most recent call last):
262+
...
263+
DoesNotExistError: Resource execution does not exist
264+
265+
>>> body = {'__type': body_type}
266+
>>> err = SWFResponseError(status, reason, body, error_code)
267+
>>> raises(DoesNotExistError,
268+
... when=is_unknown_resource_raised,
269+
... extract=extract_resource)(err)
270+
Traceback (most recent call last):
271+
...
272+
DoesNotExistError: Resource unknown does not exist
273+
274+
Now, we do the same for an unknown domain:
275+
276+
>>> body_message = 'Unknown domain'
277+
>>> body = {'__type': body_type, 'message': body_message}
278+
>>> err = SWFResponseError(status, reason, body, error_code)
279+
>>> raises(DoesNotExistError,
280+
... when=is_unknown_resource_raised,
281+
... extract=extract_resource)(err)
282+
Traceback (most recent call last):
283+
...
284+
DoesNotExistError: Resource domain does not exist
285+
286+
If it does not detect an error related to an unknown resource,
287+
it raises a :class:`ResponseError`:
288+
289+
>>> body_message = 'Other Fault'
290+
>>> body = {'__type': body_type, 'message': body_message}
291+
>>> err = SWFResponseError(status, reason, body, error_code)
292+
>>> err.error_code = 'OtherFault'
293+
>>> raises(DoesNotExistError,
294+
... when=is_unknown_resource_raised,
295+
... extract=extract_resource)(err)
296+
Traceback (most recent call last):
297+
...
298+
SWFResponseError: SWFResponseError: 400 Bad Request
299+
{'message': 'Other Fault', '__type': 'com.amazonaws.swf.base.model#UnknownResourceFault'}
300+
301+
If it's not a :class:`boto.swf.exceptions.SWFResponseError`, it
302+
raises the exception as-is:
303+
304+
>>> raises(DoesNotExistError,
305+
... when=is_unknown_resource_raised,
306+
... extract=extract_resource)(Exception('boom!'))
307+
Traceback (most recent call last):
308+
...
309+
Exception: boom!
310+
311+
"""
312+
@wraps(raises)
313+
def raises_closure(error, *args, **kwargs):
314+
if when(error, *args, **kwargs) is True:
315+
raise exception(extract(error))
316+
raise error
317+
return raises_closure
318+
319+
320+
def catch(exceptions, handle_with=None, log=False):
321+
"""
322+
Catch *exceptions*, then eventually handle and log them.
323+
324+
:param exceptions: sequence of exceptions to catch.
325+
:type exceptions: tuple
326+
327+
:param handle_with: handle the exceptions (if handle_with is not None) or
328+
raise them again.
329+
:type handle_with: function(err, *args, **kwargs)
330+
331+
:param log: the exception with default logger.
332+
:type log: bool
333+
334+
Examples:
335+
336+
>>> def boom():
337+
... raise ValueError('test')
338+
>>> func = catch(ValueError)(boom)
339+
>>> func()
340+
Traceback (most recent call last):
341+
...
342+
ValueError: test
343+
>>> func = catch(ValueError, handle_with=ignore)(boom)
344+
>>> func()
345+
>>> func = catch([ValueError], handle_with=ignore)(boom)
346+
>>> func()
347+
348+
"""
349+
def wrap(func):
350+
@wraps(func)
351+
def decorated(*args, **kwargs):
352+
try:
353+
return func(*args, **kwargs)
354+
except exceptions as err:
355+
if log is True:
356+
logger = logging.getLogger(__name__)
357+
358+
logger.error('call to {} raised: {}'.format(
359+
func.__name__,
360+
err))
361+
362+
if handle_with is None:
363+
raise
364+
365+
return handle_with(err, *args, **kwargs)
366+
367+
return decorated
368+
369+
if not isinstance(exceptions, collections.Sequence):
370+
exceptions = tuple([exceptions])
371+
elif not isinstance(exceptions, tuple):
372+
exceptions = tuple(exceptions)
373+
374+
return wrap
375+
376+
377+
when = catch
378+
379+
380+
is_not = partial(catch, handle_with=always(False))
381+
is_not.__doc__ = """
382+
Return ``False`` if it catches an exception among *exceptions*.
383+
"""
384+
385+
386+
def translate(exceptions, to):
387+
"""
388+
Catches an exception among *exceptions* and raise *to* instead.
389+
390+
"""
391+
def throw(err, *args, **kwargs):
392+
raise to(err.message)
393+
394+
return catch(exceptions, handle_with=throw)

0 commit comments

Comments
 (0)