+ return deprecated_decorator
+
+
+def truncate(text, max_length=1024):
+ """Limit huge texts in number of characters"""
+ text = str(text)
+ if text and len(text) >= max_length:
+ return text[:max_length//2-3] + " ... " + text[-max_length//2+3:]
+ return text
+
+
+def merge_dicts(*dicts, **kwargs):
+ """Creates a new dict merging N others and keyword arguments.
+ Right-most dicts take precedence.
+ Keyword args take precedence.
+ """
+ return reduce(
+ lambda acc, x: acc.update(x) or acc,
+ list(dicts) + [kwargs], {})
+
+
+def remove_none_items(adict):
+ """Return a similar dict without keys associated to None values"""
+ return {k: v for k, v in adict.items() if v is not None}
+
+
+def filter_dict_keys(adict, allow):
+ """Return a similar dict, but just containing the explicitly allowed keys
+
+ Arguments:
+ adict (dict): Simple python dict data struct
+ allow (list): Explicits allowed keys
+ """
+ return {k: v for k, v in adict.items() if k in allow}
+
+
+def filter_out_dict_keys(adict, deny):
+ """Return a similar dict, but not containing the explicitly denied keys
+
+ Arguments:
+ adict (dict): Simple python dict data struct
+ deny (list): Explicits denied keys
+ """
+ return {k: v for k, v in adict.items() if k not in deny}
+
+
+def expand_joined_fields(record):
+ """Given a db query result, explode the fields that contains `.` (join
+ operations).
+
+ Example
+ >> expand_joined_fiels({'wim.id': 2})
+ # {'wim': {'id': 2}}
+ """
+ result = {}
+ for field, value in record.items():
+ keys = field.split('.')
+ target = result
+ target = reduce(lambda target, key: target.setdefault(key, {}),
+ keys[:-1], result)
+ target[keys[-1]] = value
+
+ return result
+
+
+def ensure(condition, exception):
+ """Raise an exception if condition is not met"""
+ if not condition:
+ raise exception
+
+
+def partition(predicate, iterable):
+ """Create two derived iterators from a single one
+ The first iterator created will loop thought the values where the function
+ predicate is True, the second one will iterate over the values where it is
+ false.
+ """
+ iterable1, iterable2 = tee(iterable)
+ return filter(predicate, iterable2), filterfalse(predicate, iterable1)
+
+
+def pipe(*functions):
+ """Compose functions of one argument in the opposite order,
+ So pipe(f, g)(x) = g(f(x))
+ """
+ return lambda x: reduce(lambda acc, f: f(acc), functions, x)
+
+
+def compose(*functions):
+ """Compose functions of one argument,
+ So compose(f, g)(x) = f(g(x))
+ """
+ return lambda x: reduce(lambda acc, f: f(acc), functions[::-1], x)
+
+
+def safe_get(target, key_path, default=None):
+ """Given a path of keys (eg.: "key1.key2.key3"), return a nested value in
+ a nested dict if present, or the default value
+ """
+ keys = key_path.split('.')
+ target = reduce(lambda acc, key: acc.get(key) or {}, keys[:-1], target)
+ return target.get(keys[-1], default)
+
+
+class Attempt(object):
+ """Auxiliary class to be used in an attempt to retry executing a failing
+ procedure
+
+ Attributes:
+ count (int): 0-based "retries" counter
+ max_attempts (int): maximum number of "retries" allowed
+ info (dict): extra information about the specific attempt
+ (can be used to produce more meaningful error messages)
+ """
+ __slots__ = ('count', 'max', 'info')
+
+ MAX = 3
+
+ def __init__(self, count=0, max_attempts=MAX, info=None):
+ self.count = count
+ self.max = max_attempts
+ self.info = info or {}
+
+ @property
+ def countdown(self):
+ """Like count, but in the opposite direction"""
+ return self.max - self.count
+
+ @property
+ def number(self):
+ """1-based counter"""
+ return self.count + 1
+
+
+def inject_args(fn=None, **args):
+ """Partially apply keyword arguments in a function, but only if the function
+ define them in the first place
+ """
+ if fn is None: # Allows calling the decorator directly or with parameters
+ return partial(inject_args, **args)
+
+ spec = getspec(fn)
+ return wraps(fn)(partial(fn, **filter_dict_keys(args, spec.args)))
+
+
+def get_arg(name, fn, args, kwargs):
+ """Find the value of an argument for a function, given its argument list.
+
+ This function can be used to display more meaningful errors for debugging
+ """
+ if name in kwargs:
+ return kwargs[name]
+
+ spec = getspec(fn)
+ if name in spec.args:
+ i = spec.args.index(name)
+ return args[i] if i < len(args) else None
+
+ return None