Versatile async-friendly library to retry failed operations with configurable backoff strategies

Overview

riprova Build Status PyPI Coverage Status Documentation Status Quality Versions

riprova (meaning retry in Italian) is a small, general-purpose and versatile Python library that provides retry mechanisms with multiple backoff strategies for any sort of failed operations.

It's domain agnostic, highly customizable, extensible and provides a minimal API that's easy to instrument in any code base via decorators, context managers or raw API consumption.

For a brief introduction about backoff mechanisms for potential failed operations, read this article.

Features

  • Retry decorator for simple and idiomatic consumption.
  • Simple Pythonic programmatic interface.
  • Maximum retry timeout support.
  • Supports error whitelisting and blacklisting.
  • Supports custom error evaluation retry logic (useful to retry only in specific cases).
  • Automatically retry operations on raised exceptions.
  • Supports asynchronous coroutines with both async/await and yield from syntax.
  • Configurable maximum number of retry attempts.
  • Highly configurable supporting max retries, timeouts or retry notifier callback.
  • Built-in backoff strategies: constant, fibonacci and exponential backoffs.
  • Supports sync/async context managers.
  • Pluggable custom backoff strategies.
  • Lightweight library with almost zero embedding cost.
  • Works with Python +2.6, 3.0+ and PyPy.

Backoff strategies

List of built-in backoff strategies.

You can also implement your own one easily. See ConstantBackoff for an implementation reference.

Installation

Using pip package manager (requires pip 1.9+. Upgrade it running: pip install -U pip):

pip install -U riprova

Or install the latest sources from Github:

pip install -e git+git://github.com/h2non/riprova.git#egg=riprova

API

Examples

You can see more featured examples from the documentation site.

Basic usage examples:

import riprova

@riprova.retry
def task():
    """Retry operation if it fails with constant backoff (default)"""

@riprova.retry(backoff=riprova.ConstantBackoff(retries=5))
def task():
    """Retry operation if it fails with custom max number of retry attempts"""

@riprova.retry(backoff=riprova.ExponentialBackOff(factor=0.5))
def task():
    """Retry operation if it fails using exponential backoff"""

@riprova.retry(timeout=10)
def task():
    """Raises a TimeoutError if the retry loop exceeds from 10 seconds"""

def on_retry(err, next_try):
    print('Operation error: {}'.format(err))
    print('Next try in: {}ms'.format(next_try))

@riprova.retry(on_retry=on_retry)
def task():
    """Subscribe via function callback to every retry attempt"""

def evaluator(response):
    # Force retry operation if not a valid response
    if response.status >= 400:
        raise RuntimeError('invalid response status')  # or simple return True
    # Otherwise return False, meaning no retry
    return False

@riprova.retry(evaluator=evaluator)
def task():
    """Use a custom evaluator function to determine if the operation failed or not"""

@riprova.retry
async def task():
    """Asynchronous coroutines are also supported :)"""

Retry failed HTTP requests:

import pook
import requests
from riprova import retry

# Define HTTP mocks to simulate failed requests
pook.get('server.com').times(3).reply(503)
pook.get('server.com').times(1).reply(200).json({'hello': 'world'})


# Retry evaluator function used to determine if the operated failed or not
def evaluator(response):
    if response != 200:
        return Exception('failed request')  # you can also simply return True
    return False


# On retry even subscriptor
def on_retry(err, next_try):
    print('Operation error {}'.format(err))
    print('Next try in {}ms'.format(next_try))


# Register retriable operation
@retry(evaluator=evaluator, on_retry=on_retry)
def fetch(url):
    return requests.get(url)


# Run task that might fail
fetch('http://server.com')

License

MIT - Tomas Aparicio

Comments
  • Fault result

    Fault result

    Don't really know does it fits into your library (I mean, you decide what is relevant), but when I wrote my own «restarter» I use some fault_result argument, which returned as function result after attempts gone. In my case this was made to avoid try/except/pass things, cause function fault was like «Oh, fault! Ok, will call it later anyway, does not matter…». It looks like right now we can't do that without external try/except/pass wrapper.

    enhancement 
    opened by pohmelie 11
  • How to use retrier inside class function with custom on_retry?

    How to use retrier inside class function with custom on_retry?

    Hi, I'd like to use riprova.Retrier inside my custom class. e.g. I want to try to re-login every time say_trier fails or raises an exception. Is this sample code right? Didn't find a nice way to use decorater for say_trier.

    
    class SayHello(object):
        def login(self):
            print "log in"
            do_login_process()
    
        def say(self):
            retrier = riprova.Retrier(backoff=riprova.ExponentialBackOff(interval=30), on_retry=self.on_retry)
            retrier.run(self.say_trier)
    
        def say_trier(self):
            something_may_fail_here()
    
        def on_retry(self, err, next_try):
            print('Operation error: {}'.format(err))
            print('Next try in: {}ms'.format(next_try))
            self.login()
    
    sa = SayHello()
    sa.say()
    
    question 
    opened by dofine 5
  • Exclude paco

    Exclude paco

    I see nothing wrong with using third party libraries, but in some cases it looks like is-number npm package :laughing:. It means, that js is poor, since it need such third-party «patches». I don't want to think, that python is poor, since it is not.

    • paco.wraps should be removed cause it's enough to wrap functions with asyncio.coroutine without inspecting it.
    >>> def sync(): print("sync")
    ... 
    >>> async def async_(): print("async")
    ... 
    >>> asyncio.get_event_loop().run_until_complete(asyncio.coroutine(sync)())
    sync
    >>> asyncio.get_event_loop().run_until_complete(asyncio.coroutine(async_)())
    async
    >>>
    
    • paco.TimeoutLimit, async_timeout used when you have couple of things inside with block. If you have one coroutine, which you want to limit, you should use asyncio.wait_for and you don't need to split logic for running with or without timeout. If there is no timeout, then your timeout should be None.

    I hope this two suggestions are strong enough to exclude paco from dependencies.

    enhancement 
    opened by pohmelie 5
  • Retry context manager

    Retry context manager

    with riprova.Retrier(backoff=ConstantBackoff()) as retry:
       retry.run(run_failed_task, 'foo', bar=1)
    
    async with riprova.AsyncRetrier(backoff=ConstantBackoff()) as retry:
       await retry.run(run_failed_task, 'foo', bar=1)
    
    enhancement 
    opened by tomas-fp 4
  • Re-raise exception instance instead of creating a new exception with no args

    Re-raise exception instance instead of creating a new exception with no args

    Currently, the context manager re-raises exception by its type with no args. So, there is no error message anymore. Besides it In some cases, args are mandatory and such behavior may lead to weird error messages. So, we must re-raise an existing exception instance instead of creating the new one.

    opened by ffix 3
  • Cannot retry when `asyncio.TimeoutError` is raised

    Cannot retry when `asyncio.TimeoutError` is raised

    riprova cannot retry when a method raise asyncio.TimeoutError, even when asyncio.TimeoutError is set in blacklist. The reason is that asyncio.TimeoutError is handle explicitly in https://github.com/h2non/riprova/blob/v0.2.5/riprova/async_retrier.py#L238-L240, thus it cannot trigger retry. Is this an intended behavior?

    The following is sample code to reprocedure this issue.

    import asyncio
    import riprova
    
    BLACKLIST = riprova.ErrorBlacklist([
        asyncio.TimeoutError,
    ])
    
    @riprova.retry(backoff=riprova.ConstantBackoff(interval=1, retries=3), error_evaluator=BLACKLIST.isretry)
    async def func():
        print('func')
        raise asyncio.TimeoutError()
    
    asyncio.get_event_loop().run_until_complete(func())
    

    The func will only run once, instead of three times.

    opened by czchen 3
  • Allow newer six

    Allow newer six

    six 1.11 (which has been available for half a year now) doesn't have any backwards incompatible changes that I know of and some other libraries (like CherryPy) depend on newer six which makes it difficult to use them together. Let's relax the version restriction to support such installations.

    enhancement 
    opened by jstasiak 3
  • Bug | Inconsistent test

    Bug | Inconsistent test

    timeout = 200 while interval=100 causes the target function to run either twice or thrice (due to machine speed), which makes the following test inconsistent:

    def test_retrier_run_max_timeout(MagicMock):
        iterable = (ValueError, NotImplementedError, RuntimeError, Exception)
        task = MagicMock(side_effect=iterable)
    
        retrier = Retrier(timeout=200, backoff=ConstantBackoff(interval=100))
    
        with pytest.raises(RetryTimeoutError):
            retrier.run(task, 2, 4, foo='bar')
    
        assert task.called
        assert task.call_count >= 1
        task.assert_called_with(2, 4, foo='bar')
    
        assert retrier.attempts >= 1
        assert isinstance(retrier.error, NotImplementedError)
    

    Sometimes the raised error would be NotImplementedError, while at other times RunTimeError

    I changed this test in my pull request but the core problem needs to be fixed.

    bug 
    opened by tsarpaul 3
  • Retrier catches internal error

    Retrier catches internal error

    
     # Get delay before next retry
     delay = self.backoff.next()
    
     # If backoff is ready
     if delay == Backoff.STOP:
     return raise_from(MaxRetriesExceeded('max retries exceeded'), err)
    

    While messing around with the code I checked err value and was surprised to see ImportError:

    err = {ImportError} No module named stackless

    The error was PyCharm related, maybe a similar error would be caused if I forgot to pip install requirements?

    This code ignores all errors, even if it is caused internally and not by the function.

    A possible solution is to blacklist certain errors, such as ImportError

    enhancement 
    opened by tsarpaul 3
  • Can't pass sleep_fn from retry to Retrier

    Can't pass sleep_fn from retry to Retrier

    >>> import riprova
    >>> import time
    >>> def my_sleep(*args, **kwargs): print("my sleep", args, kwargs); time.sleep(*args, **kwargs)
    ... 
    >>> my_sleep(1)
    my sleep (1,) {}
    >>> def fail(): raise Exception
    ... 
    >>> riprova.retry(backoff=riprova.ConstantBackoff(interval=2, retries=5), sleep_fn=my_sleep)(fail)()
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
        riprova.retry(backoff=riprova.ConstantBackoff(interval=2, retries=5), sleep_fn=my_sleep)(fail)()
      File "/home/poh/pro/py/riprova/riprova/retry.py", line 132, in wrapper
        return retry_runner(*args, **kw)
      File "/home/poh/pro/py/riprova/riprova/retrier.py", line 294, in run
        delay = self._get_delay()
      File "/home/poh/pro/py/riprova/riprova/retrier.py", line 246, in _get_delay
        self.error)
      File "<string>", line 2, in raise_from
    riprova.exceptions.MaxRetriesExceeded: max retries exceeded
    >>> 
    

    The reason is identical names for keyword only arguments for decorator and wrapper: https://github.com/h2non/riprova/blob/master/riprova/retry.py#L24 https://github.com/h2non/riprova/blob/master/riprova/retry.py#L120

    opened by pohmelie 2
  • Feature: support custom error exception evaluator

    Feature: support custom error exception evaluator

    The API might look like:

    def error_evaluator(err):
       return not isinstance(err, (MyCustomError, AnotherCustomError))
    
    @riprova.retry(error_evaluator=error_evaluator)
    def task():
       pass
    
    enhancement 
    opened by h2non 1
  • Deprecation warnings

    Deprecation warnings

    Few deprecation warnings. FYI.

    /usr/local/lib/python3.8/site-packages/riprova/async_retrier.py:141: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
    self.on_retry = asyncio.coroutine(on_retry) if on_retry else None
    
    /usr/local/lib/python3.8/site-packages/riprova/async_retrier.py:279: DeprecationWarning: The loop argument is deprecated since Python 3.8, and scheduled for removal in Python 3.10.
    return (yield from asyncio.wait_for(
    
    opened by alekna 0
  • asyncio concurrent asyncio retry does not work.

    asyncio concurrent asyncio retry does not work.

    When calling asyncio.gather with functions decorated retry, the retries would finish earlier than expected.

    import asyncio
    import logging
    
    import riprova
    
    
    logging.basicConfig(
        format='[%(asctime)s] %(levelname)s - %(message)s',
        datefmt="%Y-%m-%d %H:%M:%S",
        level=logging.INFO
    )
    LOG = logging.getLogger()
    
    
    def do_retry(val):
        LOG.info("do retry %s", val)
        return True
    
    
    @riprova.retry(
        backoff=riprova.ConstantBackoff(retries=5),
        error_evaluator=do_retry,
    )
    async def do_stuff(name):
        LOG.info('calling %s', name)
        if name == 'bar':
            await asyncio.sleep(1.0)
        else:
            await asyncio.sleep(0.1)
    
        raise ValueError('value-error-{}'.format(name))
    
    
    async def process():
        coros = [do_stuff('foo'), do_stuff('bar')]
        # coros = [do_stuff('foo')]
        await asyncio.gather(*coros, return_exceptions=True)
    
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(process())
    loop.close()
    

    results:

    [2018-03-29 19:17:42] INFO - calling bar
    [2018-03-29 19:17:42] INFO - calling foo
    [2018-03-29 19:17:42] INFO - do retry value-error-foo
    [2018-03-29 19:17:42] INFO - calling foo
    [2018-03-29 19:17:43] INFO - do retry value-error-foo
    [2018-03-29 19:17:43] INFO - calling foo
    [2018-03-29 19:17:43] INFO - do retry value-error-foo
    [2018-03-29 19:17:43] INFO - calling foo
    [2018-03-29 19:17:43] INFO - do retry value-error-foo
    [2018-03-29 19:17:43] INFO - calling foo
    [2018-03-29 19:17:43] INFO - do retry value-error-foo
    [2018-03-29 19:17:43] INFO - do retry value-error-bar
    [2018-03-29 19:17:43] INFO - calling foo
    [2018-03-29 19:17:43] INFO - do retry value-error-foo
    

    The expected result is that foo and bar should be both called 6x.

    question 
    opened by bt-wil 0
Owner
Tom
Computers harasser
Tom
An open source, multipurpose, configurable discord bot that does it all

Spacebot - Discord Bot Music, Moderation, Fun, Utilities, Games and Fully Configurable. Overview • Contributing • Self hosting • Documentation (not re

Dhravya Shah 41 Dec 10, 2022
An open-source, multipurpose, configurable discord bot that does it all

Spacebot is an open source discord bot that is designed to be fun, easy to use, and replace every other discord bot out there!! Feel free to add a star ⭐ to the repository to promote the project!

Dhravya Shah 41 Dec 10, 2022
The most versatile torrent leecher and youtube-dl bot for telegram

TorToolkit Telegram So basically Tortoolkit is aimed to be the most versatile torrent leecher and youtube-dl bot for telegram. This bot is highly cust

αвιנтн 1 Nov 11, 2021
Scratch2py or S2py is a easy to use, versatile tool to communicate with the Scratch API Based of Scratch2py

Scratch2py Scratch2py or S2py is a easy to use, versatile tool to communicate with the Scratch API Based of Scratch2py Installation Run this command i

null 2 Jan 13, 2022
Revolt.py - An async library to interact with the https://revolt.chat api.

Revolt.py An async library to interact with the https://revolt.chat api. This library will be focused on making bots and i will not implement anything

Zomatree 0 Oct 8, 2022
Utilizing the freqtrade high-frequency cryptocurrency trading framework to build and optimize trading strategies. The bot runs nonstop on a Rasberry Pi.

Freqtrade Strategy Repository Please test all scripts and dry run them before using them in live mode Contact me on discord if you have any questions!

Michael Fourie 90 Jan 1, 2023
Tools for use in DeFi. Impermanent Loss calculations, staking and farming strategies, coingecko and pancakeswap API queries, liquidity pools and more

DeFi open source tools Get Started Instalation General Tools Impermanent Loss, simple calculation Compare Buy & Hold with Staking and Farming Complete

Juan Pablo Pisano 467 Jan 8, 2023
A Pancakeswap v2 trading client (and bot) with limit orders, stop-loss, custom gas strategies, a GUI and much more.

Pancakeswap v2 trading client A Pancakeswap trading client (and bot) with limit orders, stop-loss, custom gas strategies, a GUI and much more. If you

null 571 Mar 15, 2022
A Pancakeswap and Uniswap trading client (and bot) with limit orders, marker orders, stop-loss, custom gas strategies, a GUI and much more.

Pancakeswap and Uniswap trading client Adam A A Pancakeswap and Uniswap trading client (and bot) with market orders, limit orders, stop-loss, custom g

null 570 Mar 9, 2022
Recommendation systems are among most widely preffered marketing strategies.

Recommendation systems are among most widely preffered marketing strategies. Their popularity comes from close prediction scores obtained from relationships of users and items. In this project, two recommendation systems are used for two different datasets: Association Recommendation Learning and Collaborative Filtering. Please read the description for more info.

Sübeyte 8 Oct 6, 2021
Framework for creating and running trading strategies. Blatantly stolen copy of qtpylib to make it work for Indian markets.

>_• Kinetick Trade Bot Kinetick is a framework for creating and running trading strategies without worrying about integration with broker and data str

Vinay 41 Dec 31, 2022
Trading Strategies (~50%) developed by GreenT on QuantConnect platform over the autumn quarter

Trading Strategies ~50% of codes from the Applied Financial Technology Course. Contributors: Claire W. Derrick T. Frank L. Utkarsh T. Course Leads: Dy

Utkarsh 2 Feb 7, 2022
Cryptocurrency Trading Bot - A trading bot to automate cryptocurrency trading strategies using Python, equipped with a basic GUI

Cryptocurrency Trading Bot - A trading bot to automate cryptocurrency trading strategies using Python, equipped with a basic GUI. Used REST and WebSocket API to connect to two of the most popular crypto exchanges in the world.

Francis 8 Sep 15, 2022
Discord bot for calculating basic operations and formulas. (Early Development)

MathBot Discord bot for calculating basic operations and formulas. (Early Development) Commits Feel free to contribute to this bot by forking and pull

null 4 Jul 14, 2022
Mixcloud API wrapper for Python and Async IO

aiomixcloud Mixcloud API wrapper for Python and Async IO aiomixcloud is a wrapper library for the HTTP API of Mixcloud. It supports asynchronous opera

Aristotelis Mikropoulos 26 Dec 31, 2022
Async client API for the Telegram Group Calls

PyTgCalls This project allow to make Telegram group call with MTProto Api using Pyrogram and WebRTC, this is possible thanks to the power of NodeJS's

null 185 Jan 3, 2023
This is a simple unofficial async Api-wrapper for tio.run

Async-Tio This is a simple unofficial async Api-wrapper for tio.run

Tom-the-Bomb 7 Oct 28, 2022
Async ready API wrapper for Revolt API written in Python.

Mutiny Async ready API wrapper for Revolt API written in Python. Installation Python 3.9 or higher is required To install the library, you can just ru

null 16 Mar 29, 2022
A modern, easy to use, feature-rich, and async ready API wrapper improved and revived from original discord.py.

A Python API wrapper that is improved and revived from the original discord.py

Orion 19 Nov 6, 2021