:param func (callable):
            The function to retry. No arguments are passed to this function.
            If your function requires arguments, consider defining a separate
            function or use functools.partial / a lambda function.

        :param max_attempts:
            Any integer number to limit the maximum number of attempts.
            Set to None for unlimited retries.

        :param exceptions:
            A tuple of exceptions that should result in a retry. Catches
            everything derived from 'Exception' by default.

        :param wait:
            This can be an integer / float to specify the waittime in seconds
            before the next attempt. You can also pass a function which accepts
            a single argument 'attempt'.

        :param cleanup_hook:
            Can be set to a callable and will be called after an exception is
            raised from calling `func`.
            No arguments are passed to this function.
            If your function requires arguments, consider defining a separate
            function or use functools.partial / a lambda function.

        :param pre_retry_hook:
            Can be set to any callable that will be called before `function`
            is called.
            No arguments are passed to this function.
            If your function requires arguments, consider defining a separate
            function or use functools.partial / a lambda function.
            If `wait` is set, `pre_retry_hook` will be called before the
            waittime.
            Exceptions that are raised when calling this hook are not caught.

        :returns:
            The result of calling the given `func`.

        :raises:
            Any exception which is
             - not in the given `exceptions`
             - raised in `pre_retry_hook` or in `cleanup_hook`
             - raised in the last attempt at calling `func`

def call(func, max_attempts=None, exceptions=Exception, wait=0.0,
         cleanup_hook=None, pre_retry_hook=None):
    """ :param func (callable):
            The function to retry. No arguments are passed to this function.
            If your function requires arguments, consider defining a separate
            function or use functools.partial / a lambda function.

        :param max_attempts:
            Any integer number to limit the maximum number of attempts.
            Set to None for unlimited retries.

        :param exceptions:
            A tuple of exceptions that should result in a retry. Catches
            everything derived from 'Exception' by default.

        :param wait:
            This can be an integer / float to specify the waittime in seconds
            before the next attempt. You can also pass a function which accepts
            a single argument 'attempt'.

        :param cleanup_hook:
            Can be set to a callable and will be called after an exception is
            raised from calling `func`.
            No arguments are passed to this function.
            If your function requires arguments, consider defining a separate
            function or use functools.partial / a lambda function.

        :param pre_retry_hook:
            Can be set to any callable that will be called before `function`
            is called.
            No arguments are passed to this function.
            If your function requires arguments, consider defining a separate
            function or use functools.partial / a lambda function.
            If `wait` is set, `pre_retry_hook` will be called before the
            waittime.
            Exceptions that are raised when calling this hook are not caught.

        :returns:
            The result of calling the given `func`.

        :raises:
            Any exception which is
             - not in the given `exceptions`
             - raised in `pre_retry_hook` or in `cleanup_hook`
             - raised in the last attempt at calling `func`
    """
    # we check the callables in advance to prevent raising exceptions
    # after making the first attempt
    _assert_callable(func, allow_none=False)
    _assert_callable(cleanup_hook, allow_none=True)
    _assert_callable(pre_retry_hook, allow_none=True)
    if not (max_attempts is None or max_attempts >= 1):
        raise ValueError('max_attempts must be None or an integer >= 1')

    # if the user sets the waittime to a fixed value (int or float) we create
    # a function which always returns this fixed value. This way we avoid
    # having to make this decision in the retry loop.
    wait_func = wait if type(wait) not in [int, float] else lambda _: wait
    _assert_callable(wait_func, allow_none=False)

    def log_failed_attempt(attempt, error):
        if max_attempts is None:
            nr_display = '{0}'.format(attempt)
        else:
            nr_display = '{0} / {1}'.format(attempt, max_attempts)
        logger.debug('Attempt {nr} at calling {func} failed ({msg})'
                     .format(nr=nr_display, func=func, msg=error))

    for attempt, f in enumerate(_repeat(func, max_attempts), start=1):
        try:
            return f()
        except exceptions as e:
            log_failed_attempt(attempt=attempt, error=e)
            if cleanup_hook is not None:
                cleanup_hook()
            if attempt == max_attempts:
                raise
            if wait:
                waittime = wait_func(attempt)
                time.sleep(waittime)
            if pre_retry_hook is not None:
                pre_retry_hook()