"""
General utility methods
"""
from __future__ import (absolute_import, division,
print_function, unicode_literals)
import errno
from functools import wraps
import os
import re
from future.builtins import ( # noqa
bytes, dict, int, list, object, range, str,
ascii, chr, hex, input, next, oct, open,
pow, round, super,
filter, map, zip)
import copy
import signal
import time
import math
INTERVAL_FORMAT = '^\\s*(\d+)(ms|h|m|s|d|w)\\s*$'
[docs]def dict_merge(*dictionaries):
"""
Performs nested merge of multiple dictionaries. The values from
dictionaries appearing first takes precendence
:param dictionaries: List of dictionaries that needs to be merged.
:return: merged dictionary
:rtype
"""
merged_dict = {}
def merge(source, defaults):
source = copy.deepcopy(source)
# Nested merge requires both source and defaults to be dictionary
if isinstance(source, dict) and isinstance(defaults, dict):
for key, value in defaults.items():
if key not in source:
# Key not found in source : Use the defaults
source[key] = value
else:
# Key found in source : Recursive merge
source[key] = merge(source[key], value)
return source
for merge_with in dictionaries:
merged_dict = merge(merged_dict, copy.deepcopy(merge_with or {}))
return merged_dict
[docs]class TimeoutError(Exception):
"""
Error corresponding to timeout of a function use with @timeout annotation.
"""
pass
[docs]def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
"""
Decorator that applies timeout for a dunction
:param seconds: Timeout in seconds. Defaults to 10s.
:param error_message: Error message corresponding to timeout.
:return: decorated function
"""
def decorator(func):
def _handle_timeout(signum, frame):
raise TimeoutError(error_message)
@wraps(func)
def wrapper(*args, **kwargs):
try:
signal.signal(signal.SIGALRM, _handle_timeout)
signal_disabled = False
except ValueError:
# Possibly running in debug mode. Timeouts will be ignored
signal_disabled = True
pass
if not signal_disabled:
signal.alarm(seconds)
try:
result = func(*args, **kwargs)
finally:
if not signal_disabled:
signal.alarm(0)
return result
return wrapper
return decorator
[docs]def function_retry(tries, delay, backoff, except_on, fn, *args, **kwargs):
mtries, mdelay = tries, delay # make mutable
while True:
try:
return fn(*args, **kwargs)
except except_on:
pass
mtries -= 1 # consume an attempt
if mtries > 0:
time.sleep(mdelay) # wait...
mdelay *= backoff # make future wait longer
# Re-raise last exception
raise
# Retry decorator with backoff
[docs]def retry(tries, delay=3, backoff=2, except_on=(Exception, )):
"""Retries a function or method until it returns True.
delay sets the initial delay in seconds, and backoff sets the factor by
which the delay should lengthen after each failure.
tries must be at least 0, and delay greater than 0."""
tries = math.floor(tries)
def decorator(f):
def f_retry(*args, **kwargs):
return function_retry(
tries, delay, backoff, except_on, f, *args, **kwargs)
return f_retry # true decorator -> decorated function
return decorator # @retry(arg[, ...]) -> true decorator
[docs]def to_milliseconds(interval):
"""
Converts string interval to milliseoncds
:param interval: Time interval represented in string format. (.e.g: 5s)
:type interval: str
:return: Interval in milliseconds
:rtype: long
"""
match = re.search(INTERVAL_FORMAT, interval)
if match and len(match.groups()) == 2:
# Suffix can be 'h' , 'm', 's' or 'ms'
suffix = match.group(2)
prefix = int(match.group(1))
converter = {
's': lambda use_prefix: use_prefix * 1000,
'm': lambda use_prefix: use_prefix * 60 * 1000,
'h': lambda use_prefix: use_prefix * 60 * 60 * 1000,
'd': lambda use_prefix: use_prefix * 24 * 60 * 60 * 1000,
'w': lambda use_prefix: use_prefix * 07 * 24 * 60 * 60 * 1000,
}.get(suffix, lambda use_prefix: use_prefix)
return converter(prefix)
else:
# Invalid interval. Raise exception.
raise InvalidInterval(interval)
[docs]class InvalidInterval(Exception):
"""
Exception corresponding to invalid time interval.
"""
def __init__(self, interval):
"""
:param interval: Invalid interval
:type interval: str
"""
self.message = 'Invalid interval specified:{0}. Interval should ' \
'match format: {1}'.format(interval, INTERVAL_FORMAT)
self.interval = interval
super(InvalidInterval, self).__init__(interval)
[docs] def to_dict(self):
return {
'code': 'INVALID_INTERVAL',
'message': self.message,
'details': {
'interval': self.interval
}
}
def __str__(self):
return self.message