tenacity-4.8.0/0000755000372000037200000000000013214436031014200 5ustar travistravis00000000000000tenacity-4.8.0/doc/0000755000372000037200000000000013214436031014745 5ustar travistravis00000000000000tenacity-4.8.0/doc/source/0000755000372000037200000000000013214436031016245 5ustar travistravis00000000000000tenacity-4.8.0/doc/source/conf.py0000644000372000037200000000213613214435755017562 0ustar travistravis00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os master_doc = 'index' project = "Tenacity" extensions = [ 'sphinx.ext.doctest' ] # -- Options for sphinx.ext.doctest ----------------------------------------- # doctest_default_flags = cwd = os.path.abspath(os.path.dirname(__file__)) tenacity_path = os.path.join(cwd, os.pardir, os.pardir) doctest_path = [tenacity_path] # doctest_global_setup = # doctest_global_cleanup = # doctest_test_doctest_blocks = tenacity-4.8.0/doc/source/index.rst0000644000372000037200000002071413214435755020126 0ustar travistravis00000000000000Tenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://img.shields.io/travis/jd/tenacity.svg :target: https://travis-ci.org/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from `a fork of retrying `_. The simplest use case is retrying a flaky function whenever an `Exception` occurs until a value is returned. .. testcode:: import random from tenacity import retry @retry def do_something_unreliable(): if random.randint(0, 10) > 1: raise IOError("Broken sauce, everything is hosed!!!111one") else: return "Awesome sauce!" print(do_something_unreliable()) .. testoutput:: :hide: Awesome sauce! Features -------- - Generic Decorator API - Specify stop condition (i.e. limit by number of attempts) - Specify wait condition (i.e. exponential backoff sleeping between attempts) - Customize retrying on Exceptions - Customize retrying on expected returned result - Retry on coroutines Installation ------------ To install *tenacity*, simply: .. code-block:: bash $ pip install tenacity Examples ---------- .. testsetup:: * import logging from tenacity import * class MyException(Exception): pass As you saw above, the default behavior is to retry forever without waiting when an exception is raised. .. testcode:: @retry def never_give_up_never_surrender(): print("Retry forever ignoring Exceptions, don't wait between retries") raise Exception Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. .. testcode:: @retry(stop=stop_after_attempt(7)) def stop_after_7_attempts(): print("Stopping after 7 attempts") raise Exception We don't have all day, so let's set a boundary for how long we should be retrying stuff. .. testcode:: @retry(stop=stop_after_delay(10)) def stop_after_10_s(): print("Stopping after 10 seconds") raise Exception You can combine several stop conditions by using the `|` operator: .. testcode:: @retry(stop=(stop_after_delay(10) | stop_after_attempt(5))) def stop_after_10_s_or_5_retries(): print("Stopping after 10 seconds or 5 retries") raise Exception Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. .. testcode:: @retry(wait=wait_fixed(2)) def wait_2_s(): print("Wait 2 second between retries") raise Exception Some things perform best with a bit of randomness injected. .. testcode:: @retry(wait=wait_random(min=1, max=2)) def wait_random_1_to_2_s(): print("Randomly wait 1 to 2 seconds between retries") raise Exception Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_exponential(multiplier=1, max=10)) def wait_exponential_1(): print("Wait 2^x * 1 second between each retry, up to 10 seconds, then 10 seconds afterwards") raise Exception Then again, it's also hard to beat combining fixed waits and jitter (to help avoid thundering herds) when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_fixed(3) + wait_random(0, 2)) def wait_fixed_jitter(): print("Wait at least 3 seconds, and add up to 2 seconds of random delay") raise Exception When multiple processes are in contention for a shared resource, exponentially increasing jitter helps minimise collisions. .. testcode:: @retry(wait=wait_random_exponential(multiplier=1, max=60)) def wait_exponential_jitter(): print("Randomly wait up to 2^x * 1 seconds between each retry until the range reaches 60 seconds, then randomly up to 60 seconds afterwards") raise Exception Sometimes it's necessary to build a chain of backoffs. .. testcode:: @retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)])) def wait_fixed_chained(): print("Wait 3s for 3 attempts, 7s for the next 2 attempts and 9s for all attempts thereafter") raise Exception We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. .. testcode:: @retry(retry=retry_if_exception_type(IOError)) def might_io_error(): print("Retry forever with no wait if an IOError occurs, raise any other errors") raise Exception We can also use the result of the function to alter the behavior of retrying. .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=retry_if_result(is_none_p)) def might_return_none(): print("Retry with no wait if return value is None") We can also combine several conditions: .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type())) def might_return_none(): print("Retry forever ignoring Exceptions with no wait if return value is None") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. It's also possible to retry explicitly at any time by raising the `TryAgain` exception: .. testcode:: @retry def do_something(): result = something_else() if result == 23: raise TryAgain While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. testcode:: @retry(reraise=True, stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except MyException: # timed out retrying pass It's possible to execute an action before any attempt of calling the function by using the before callback function: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") In the same spirit, It's possible to execute after a call that failed: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") You can access the statistics about the retry made over a function by using the `retry` attribute attached to the function and its `statistics` attribute: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... You can change the arguments of a retry decorator as needed when calling it by using the `retry_with` function attached to the wrapped function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception.retry_with(stop=stop_after_attempt(4))() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Make the docs better (or more detailed, or more easier to read, or ...) .. _`the repository`: https://github.com/jd/tenacity tenacity-4.8.0/tenacity/0000755000372000037200000000000013214436031016020 5ustar travistravis00000000000000tenacity-4.8.0/tenacity/tests/0000755000372000037200000000000013214436031017162 5ustar travistravis00000000000000tenacity-4.8.0/tenacity/tests/__init__.py0000644000372000037200000000000013214435755021275 0ustar travistravis00000000000000tenacity-4.8.0/tenacity/tests/test_async.py0000644000372000037200000000273113214435755021727 0ustar travistravis00000000000000# coding: utf-8 # Copyright 2016 Étienne Bersac # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import unittest import six from tenacity import async from tenacity import retry from tenacity.tests.test_tenacity import NoIOErrorAfterCount def asynctest(callable_): callable_ = asyncio.coroutine(callable_) @six.wraps(callable_) def wrapper(*a, **kw): loop = asyncio.get_event_loop() return loop.run_until_complete(callable_(*a, **kw)) return wrapper @retry @asyncio.coroutine def _retryable_coroutine(thing): yield from asyncio.sleep(0.00001) thing.go() class TestAsync(unittest.TestCase): @asynctest def test_retry(self): assert asyncio.iscoroutinefunction(_retryable_coroutine) thing = NoIOErrorAfterCount(5) yield from _retryable_coroutine(thing) assert thing.counter == thing.count def test_repr(self): repr(async.AsyncRetrying()) if __name__ == '__main__': unittest.main() tenacity-4.8.0/tenacity/tests/test_tenacity.py0000644000372000037200000006510313214435755022434 0ustar travistravis00000000000000# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import time import unittest import six.moves import tenacity from tenacity import RetryError from tenacity import Retrying from tenacity import retry class TestBase(unittest.TestCase): def test_repr(self): repr(tenacity.BaseRetrying()) class TestStopConditions(unittest.TestCase): def test_never_stop(self): r = Retrying() self.assertFalse(r.stop(3, 6546)) def test_stop_any(self): s = tenacity.stop_any( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertTrue(s(4, 0.8)) self.assertTrue(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_all(self): s = tenacity.stop_all( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertFalse(s(4, 0.8)) self.assertFalse(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_or(self): s = tenacity.stop_after_delay(1) | tenacity.stop_after_attempt(4) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertTrue(s(4, 0.8)) self.assertTrue(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_and(self): s = tenacity.stop_after_delay(1) & tenacity.stop_after_attempt(4) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertFalse(s(4, 0.8)) self.assertFalse(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_after_attempt(self): r = Retrying(stop=tenacity.stop_after_attempt(3)) self.assertFalse(r.stop(2, 6546)) self.assertTrue(r.stop(3, 6546)) self.assertTrue(r.stop(4, 6546)) def test_stop_after_delay(self): r = Retrying(stop=tenacity.stop_after_delay(1)) self.assertFalse(r.stop(2, 0.999)) self.assertTrue(r.stop(2, 1)) self.assertTrue(r.stop(2, 1.001)) def test_legacy_explicit_stop_type(self): Retrying(stop="stop_after_attempt") def test_stop_func(self): r = Retrying(stop=lambda attempt, delay: attempt == delay) self.assertFalse(r.stop(1, 3)) self.assertFalse(r.stop(100, 99)) self.assertTrue(r.stop(101, 101)) class TestWaitConditions(unittest.TestCase): def test_no_sleep(self): r = Retrying() self.assertEqual(0, r.wait(18, 9879)) def test_fixed_sleep(self): r = Retrying(wait=tenacity.wait_fixed(1)) self.assertEqual(1, r.wait(12, 6546)) def test_incrementing_sleep(self): r = Retrying(wait=tenacity.wait_incrementing( start=500, increment=100)) self.assertEqual(500, r.wait(1, 6546)) self.assertEqual(600, r.wait(2, 6546)) self.assertEqual(700, r.wait(3, 6546)) def test_random_sleep(self): r = Retrying(wait=tenacity.wait_random(min=1, max=20)) times = set() for x in six.moves.range(1000): times.add(r.wait(1, 6546)) # this is kind of non-deterministic... self.assertTrue(len(times) > 1) for t in times: self.assertTrue(t >= 1) self.assertTrue(t < 20) def test_random_sleep_without_min(self): r = Retrying(wait=tenacity.wait_random(max=2)) times = set() times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) # this is kind of non-deterministic... self.assertTrue(len(times) > 1) for t in times: self.assertTrue(t >= 0) self.assertTrue(t <= 2) def test_exponential(self): r = Retrying(wait=tenacity.wait_exponential(max=100)) self.assertEqual(r.wait(1, 0), 2) self.assertEqual(r.wait(2, 0), 4) self.assertEqual(r.wait(3, 0), 8) self.assertEqual(r.wait(4, 0), 16) self.assertEqual(r.wait(5, 0), 32) self.assertEqual(r.wait(6, 0), 64) def test_exponential_with_max_wait(self): r = Retrying(wait=tenacity.wait_exponential(max=40)) self.assertEqual(r.wait(1, 0), 2) self.assertEqual(r.wait(2, 0), 4) self.assertEqual(r.wait(3, 0), 8) self.assertEqual(r.wait(4, 0), 16) self.assertEqual(r.wait(5, 0), 32) self.assertEqual(r.wait(6, 0), 40) self.assertEqual(r.wait(7, 0), 40) self.assertEqual(r.wait(50, 0), 40) def test_exponential_with_max_wait_and_multiplier(self): r = Retrying(wait=tenacity.wait_exponential( max=50, multiplier=1)) self.assertEqual(r.wait(1, 0), 2) self.assertEqual(r.wait(2, 0), 4) self.assertEqual(r.wait(3, 0), 8) self.assertEqual(r.wait(4, 0), 16) self.assertEqual(r.wait(5, 0), 32) self.assertEqual(r.wait(6, 0), 50) self.assertEqual(r.wait(7, 0), 50) self.assertEqual(r.wait(50, 0), 50) def test_legacy_explicit_wait_type(self): Retrying(wait="exponential_sleep") def test_wait_func_result(self): captures = [] def wait_capture(attempt, delay, last_result=None): captures.append(last_result) return 1 def dying(): raise Exception("Broken") r_attempts = 10 r = Retrying(wait=wait_capture, sleep=lambda secs: None, stop=tenacity.stop_after_attempt(r_attempts), reraise=True) self.assertRaises(Exception, r.call, dying) self.assertEqual(r_attempts - 1, len(captures)) self.assertTrue(all([r.failed for r in captures])) def test_wait_func(self): r = Retrying(wait=lambda attempt, delay: attempt * delay) self.assertEqual(r.wait(1, 5), 5) self.assertEqual(r.wait(2, 11), 22) self.assertEqual(r.wait(10, 100), 1000) def test_wait_combine(self): r = Retrying(wait=tenacity.wait_combine(tenacity.wait_random(0, 3), tenacity.wait_fixed(5))) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_double_sum(self): r = Retrying(wait=tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_triple_sum(self): r = Retrying(wait=tenacity.wait_fixed(1) + tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def test_wait_arbitrary_sum(self): r = Retrying(wait=sum([tenacity.wait_fixed(1), tenacity.wait_random(0, 3), tenacity.wait_fixed(5), tenacity.wait_none()])) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def _assert_range(self, wait, min_, max_): self.assertLess(wait, max_) self.assertGreaterEqual(wait, min_) def _assert_inclusive_range(self, wait, low, high): self.assertLessEqual(wait, high) self.assertGreaterEqual(wait, low) def test_wait_chain(self): r = Retrying(wait=tenacity.wait_chain( *[tenacity.wait_fixed(1) for i in six.moves.range(2)] + [tenacity.wait_fixed(4) for i in six.moves.range(2)] + [tenacity.wait_fixed(8) for i in six.moves.range(1)])) for i in six.moves.range(10): w = r.wait(i, 1) if i < 2: self._assert_range(w, 1, 2) elif i < 4: self._assert_range(w, 4, 5) else: self._assert_range(w, 8, 9) def test_wait_random_exponential(self): fn = tenacity.wait_random_exponential(0.5, 60.0) for _ in six.moves.range(1000): self._assert_inclusive_range(fn(0, 0), 0, 0.5) self._assert_inclusive_range(fn(1, 0), 0, 1.0) self._assert_inclusive_range(fn(2, 0), 0, 2.0) self._assert_inclusive_range(fn(3, 0), 0, 4.0) self._assert_inclusive_range(fn(4, 0), 0, 8.0) self._assert_inclusive_range(fn(5, 0), 0, 16.0) self._assert_inclusive_range(fn(6, 0), 0, 32.0) self._assert_inclusive_range(fn(7, 0), 0, 60.0) self._assert_inclusive_range(fn(8, 0), 0, 60.0) self._assert_inclusive_range(fn(9, 0), 0, 60.0) fn = tenacity.wait_random_exponential(10, 5) for _ in six.moves.range(1000): self._assert_inclusive_range(fn(0, 0), 0.00, 5.00) # Default arguments exist fn = tenacity.wait_random_exponential() fn(0, 0) def test_wait_random_exponential_statistically(self): fn = tenacity.wait_random_exponential(0.5, 60.0) attempt = [] for i in six.moves.range(10): attempt.append( [fn(i, 0) for _ in six.moves.range(4000)] ) def mean(lst): return float(sum(lst)) / float(len(lst)) self._assert_inclusive_range(mean(attempt[0]), 0.20, 0.30) self._assert_inclusive_range(mean(attempt[1]), 0.35, 0.65) self._assert_inclusive_range(mean(attempt[2]), 0.75, 1.25) self._assert_inclusive_range(mean(attempt[3]), 1.75, 3.25) self._assert_inclusive_range(mean(attempt[4]), 3.50, 5.50) self._assert_inclusive_range(mean(attempt[5]), 7.00, 9.00) self._assert_inclusive_range(mean(attempt[6]), 14.00, 18.00) self._assert_inclusive_range(mean(attempt[7]), 28.00, 34.00) self._assert_inclusive_range(mean(attempt[8]), 28.00, 34.00) self._assert_inclusive_range(mean(attempt[9]), 28.00, 34.00) class TestRetryConditions(unittest.TestCase): def test_retry_if_result(self): r = (tenacity.retry_if_result(lambda x: x == 1)) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) def test_retry_if_not_result(self): r = (tenacity.retry_if_not_result(lambda x: x == 1)) self.assertTrue(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, False))) def test_retry_any(self): r = tenacity.retry_any( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: x == 2)) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertTrue(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_all(self): r = tenacity.retry_all( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: isinstance(x, int))) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_and(self): r = (tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result(lambda x: isinstance(x, int))) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_or(self): r = (tenacity.retry_if_result(lambda x: x == "foo") | tenacity.retry_if_result(lambda x: isinstance(x, int))) self.assertTrue(r(tenacity.Future.construct(1, "foo", False))) self.assertFalse(r(tenacity.Future.construct(1, "foobar", False))) self.assertFalse(r(tenacity.Future.construct(1, 2.2, False))) self.assertFalse(r(tenacity.Future.construct(1, 42, True))) def _raise_try_again(self): self._attempts += 1 if self._attempts < 3: raise tenacity.TryAgain def test_retry_try_again(self): self._attempts = 0 Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never).call(self._raise_try_again) self.assertEqual(3, self._attempts) def test_retry_try_again_forever(self): def _r(): raise tenacity.TryAgain r = Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never) self.assertRaises(tenacity.RetryError, r.call, _r) self.assertEqual(5, r.statistics['attempt_number']) def test_retry_try_again_forever_reraise(self): def _r(): raise tenacity.TryAgain r = Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never, reraise=True) self.assertRaises(tenacity.TryAgain, r, _r) self.assertEqual(5, r.statistics['attempt_number']) class NoneReturnUntilAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Return None until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 return None return True class NoIOErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Raise an IOError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise IOError("Hi there, I'm an IOError") return True class NoNameErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Raise a NameError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise NameError("Hi there, I'm a NameError") return True class NameErrorUntilCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Return True until after count threshold has been crossed. Then raise a NameError. """ if self.counter < self.count: self.counter += 1 return True raise NameError("Hi there, I'm a NameError") class IOErrorUntilCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Return True until after count threshold has been crossed. Then raise an IOError. """ if self.counter < self.count: self.counter += 1 return True raise IOError("Hi there, I'm an IOError") class CustomError(Exception): """This is a custom exception class. Note that For Python 2.x, we don't strictly need to extend BaseException, however, Python 3.x will complain. While this test suite won't run correctly under Python 3.x without extending from the Python exception hierarchy, the actual module code is backwards compatible Python 2.x and will allow for cases where exception classes don't extend from the hierarchy. """ def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class NoCustomErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Raise a CustomError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 derived_message = "This is a Custom exception class" raise CustomError(derived_message) return True def current_time_ms(): return int(round(time.time() * 1000)) @retry(wait=tenacity.wait_fixed(0.05), retry=tenacity.retry_if_result(lambda result: result is None)) def _retryable_test_with_wait(thing): return thing.go() @retry(stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_result(lambda result: result is None)) def _retryable_test_with_stop(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_type(IOError)) def _retryable_test_with_exception_type_io(thing): return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(IOError)) def _retryable_test_with_exception_type_io_attempt_limit(thing): return thing.go() @retry(retry=tenacity.retry_unless_exception_type(NameError)) def _retryable_test_with_unless_exception_type_name(thing): return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_unless_exception_type(NameError)) def _retryable_test_with_unless_exception_type_name_attempt_limit(thing): return thing.go() @retry(retry=tenacity.retry_unless_exception_type()) def _retryable_test_with_unless_exception_type_no_input(thing): return thing.go() @retry def _retryable_default(thing): return thing.go() @retry() def _retryable_default_f(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_type(CustomError)) def _retryable_test_with_exception_type_custom(thing): return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(CustomError)) def _retryable_test_with_exception_type_custom_attempt_limit(thing): return thing.go() class TestDecoratorWrapper(unittest.TestCase): def test_with_wait(self): start = current_time_ms() result = _retryable_test_with_wait(NoneReturnUntilAfterCount(5)) t = current_time_ms() - start self.assertGreaterEqual(t, 250) self.assertTrue(result) def test_retry_with(self): start = current_time_ms() result = _retryable_test_with_wait.retry_with( wait=tenacity.wait_fixed(0.1))(NoneReturnUntilAfterCount(5)) t = current_time_ms() - start self.assertGreaterEqual(t, 500) self.assertTrue(result) def test_with_stop_on_return_value(self): try: _retryable_test_with_stop(NoneReturnUntilAfterCount(5)) self.fail("Expected RetryError after 3 attempts") except RetryError as re: self.assertFalse(re.last_attempt.failed) self.assertEqual(3, re.last_attempt.attempt_number) self.assertTrue(re.last_attempt.result() is None) print(re) def test_with_stop_on_exception(self): try: _retryable_test_with_stop(NoIOErrorAfterCount(5)) self.fail("Expected IOError") except IOError as re: self.assertTrue(isinstance(re, IOError)) print(re) def test_retry_if_exception_of_type(self): self.assertTrue(_retryable_test_with_exception_type_io( NoIOErrorAfterCount(5))) try: _retryable_test_with_exception_type_io(NoNameErrorAfterCount(5)) self.fail("Expected NameError") except NameError as n: self.assertTrue(isinstance(n, NameError)) print(n) self.assertTrue(_retryable_test_with_exception_type_custom( NoCustomErrorAfterCount(5))) try: _retryable_test_with_exception_type_custom( NoNameErrorAfterCount(5)) self.fail("Expected NameError") except NameError as n: self.assertTrue(isinstance(n, NameError)) print(n) def test_retry_until_exception_of_type_attempt_number(self): try: self.assertTrue(_retryable_test_with_unless_exception_type_name( NameErrorUntilCount(5))) except NameError as e: s = _retryable_test_with_unless_exception_type_name.\ retry.statistics self.assertTrue(s['attempt_number'] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_no_type(self): try: # no input should catch all subclasses of Exception self.assertTrue( _retryable_test_with_unless_exception_type_no_input( NameErrorUntilCount(5)) ) except NameError as e: s = _retryable_test_with_unless_exception_type_no_input.\ retry.statistics self.assertTrue(s['attempt_number'] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_wrong_exception(self): try: # two iterations with IOError, one that returns True _retryable_test_with_unless_exception_type_name_attempt_limit( IOErrorUntilCount(2)) self.fail("Expected RetryError") except RetryError as e: self.assertTrue(isinstance(e, RetryError)) print(e) def test_defaults(self): self.assertTrue(_retryable_default(NoNameErrorAfterCount(5))) self.assertTrue(_retryable_default_f(NoNameErrorAfterCount(5))) self.assertTrue(_retryable_default(NoCustomErrorAfterCount(5))) self.assertTrue(_retryable_default_f(NoCustomErrorAfterCount(5))) class TestBeforeAfterAttempts(unittest.TestCase): _attempt_number = 0 def test_before_attempts(self): TestBeforeAfterAttempts._attempt_number = 0 def _before(fn, attempt_number): TestBeforeAfterAttempts._attempt_number = attempt_number @retry(wait=tenacity.wait_fixed(1), stop=tenacity.stop_after_attempt(1), before=_before) def _test_before(): pass _test_before() self.assertTrue(TestBeforeAfterAttempts._attempt_number is 1) def test_after_attempts(self): TestBeforeAfterAttempts._attempt_number = 0 def _after(fn, attempt_number, trial_time_taken_ms): TestBeforeAfterAttempts._attempt_number = attempt_number @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(3), after=_after) def _test_after(): if TestBeforeAfterAttempts._attempt_number < 2: raise Exception("testing after_attempts handler") else: pass _test_after() self.assertTrue(TestBeforeAfterAttempts._attempt_number is 2) class TestReraiseExceptions(unittest.TestCase): def test_reraise_by_default(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), reraise=True) def _reraised_by_default(): calls.append('x') raise KeyError("Bad key") self.assertRaises(KeyError, _reraised_by_default) self.assertEqual(2, len(calls)) def test_reraise_from_retry_error(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2)) def _raise_key_error(): calls.append('x') raise KeyError("Bad key") def _reraised_key_error(): try: _raise_key_error() except tenacity.RetryError as retry_err: retry_err.reraise() self.assertRaises(KeyError, _reraised_key_error) self.assertEqual(2, len(calls)) def test_reraise_timeout_from_retry_error(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda x: True) def _mock_fn(): calls.append('x') def _reraised_mock_fn(): try: _mock_fn() except tenacity.RetryError as retry_err: retry_err.reraise() self.assertRaises(tenacity.RetryError, _reraised_mock_fn) self.assertEqual(2, len(calls)) def test_reraise_no_exception(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda x: True, reraise=True) def _mock_fn(): calls.append('x') self.assertRaises(tenacity.RetryError, _mock_fn) self.assertEqual(2, len(calls)) class TestStatistics(unittest.TestCase): def test_stats(self): @retry() def _foobar(): return 42 self.assertEqual({}, _foobar.retry.statistics) _foobar() self.assertEqual(1, _foobar.retry.statistics['attempt_number']) def test_stats_failing(self): @retry(stop=tenacity.stop_after_attempt(2)) def _foobar(): raise ValueError(42) self.assertEqual({}, _foobar.retry.statistics) try: _foobar() except Exception: pass self.assertEqual(2, _foobar.retry.statistics['attempt_number']) if __name__ == '__main__': unittest.main() tenacity-4.8.0/tenacity/tests/test_tornado.py0000644000372000037200000000237613214435755022265 0ustar travistravis00000000000000# coding: utf-8 # Copyright 2017 Elisey Zanko # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import unittest from tenacity import retry from tenacity import tornadoweb from tenacity.tests.test_tenacity import NoIOErrorAfterCount from tornado import gen from tornado import testing @retry @gen.coroutine def _retryable_coroutine(thing): yield gen.sleep(0.00001) thing.go() class TestTornado(testing.AsyncTestCase): @testing.gen_test def test_retry(self): assert gen.is_coroutine_function(_retryable_coroutine) thing = NoIOErrorAfterCount(5) yield _retryable_coroutine(thing) assert thing.counter == thing.count def test_repr(self): repr(tornadoweb.TornadoRetrying()) if __name__ == '__main__': unittest.main() tenacity-4.8.0/tenacity/__init__.py0000644000372000037200000002633313214435755020154 0ustar travistravis00000000000000# -*- coding: utf-8 -*- # Copyright 2017 Elisey Zanko # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. try: import asyncio except ImportError: asyncio = None try: import tornado except ImportError: tornado = None import inspect import sys import threading from concurrent import futures from monotonic import monotonic as now import six from tenacity import _utils from tenacity import wait as _wait # Import all built-in retry strategies for easier usage. from .retry import retry_all # noqa from .retry import retry_always # noqa from .retry import retry_any # noqa from .retry import retry_if_exception # noqa from .retry import retry_if_exception_type # noqa from .retry import retry_if_not_result # noqa from .retry import retry_if_result # noqa from .retry import retry_never # noqa from .retry import retry_unless_exception_type # noqa # Import all nap strategies for easier usage. from .nap import sleep # noqa from .nap import sleep_using_event # noqa # Import all built-in stop strategies for easier usage. from .stop import stop_after_attempt # noqa from .stop import stop_after_delay # noqa from .stop import stop_all # noqa from .stop import stop_any # noqa from .stop import stop_never # noqa from .stop import stop_when_event_set # noqa # Import all built-in wait strategies for easier usage. from .wait import wait_chain # noqa from .wait import wait_combine # noqa from .wait import wait_exponential # noqa from .wait import wait_fixed # noqa from .wait import wait_incrementing # noqa from .wait import wait_none # noqa from .wait import wait_random # noqa from .wait import wait_random_exponential # noqa from .wait import wait_random_exponential as wait_full_jitter # noqa # Import all built-in before strategies for easier usage. from .before import before_log # noqa from .before import before_nothing # noqa # Import all built-in after strategies for easier usage. from .after import after_log # noqa from .after import after_nothing # noqa def retry(*dargs, **dkw): """Wrap a function with a new `Retrying` object. :param dargs: positional arguments passed to Retrying object :param dkw: keyword arguments passed to the Retrying object """ # support both @retry and @retry() as valid syntax if len(dargs) == 1 and callable(dargs[0]): return retry()(dargs[0]) else: def wrap(f): if asyncio and asyncio.iscoroutinefunction(f): r = AsyncRetrying(*dargs, **dkw) elif tornado and tornado.gen.is_coroutine_function(f): r = TornadoRetrying(*dargs, **dkw) else: r = Retrying(*dargs, **dkw) return r.wraps(f) return wrap class TryAgain(Exception): """Always retry the executed function when raised.""" NO_RESULT = object() class DoAttempt(object): pass class DoSleep(float): pass _unset = object() class BaseRetrying(object): def __init__(self, sleep=sleep, stop=stop_never, wait=wait_none(), retry=retry_if_exception_type(), before=before_nothing, after=after_nothing, reraise=False): self.sleep = sleep self.stop = stop self.wait = wait self.retry = retry self.before = before self.after = after self.reraise = reraise self._local = threading.local() # This will allow for passing in the result and handling # the older versions of these functions that do not take # the prior result. self._wait_takes_result = self._waiter_takes_last_result(wait) def copy(self, sleep=_unset, stop=_unset, wait=_unset, retry=_unset, before=_unset, after=_unset, reraise=_unset): """Copy this object with some parameters changed if needed.""" return self.__class__( sleep=self.sleep if sleep is _unset else sleep, stop=self.stop if stop is _unset else stop, wait=self.wait if wait is _unset else wait, retry=self.retry if retry is _unset else retry, before=self.before if before is _unset else before, after=self.after if after is _unset else after, reraise=self.reraise if after is _unset else reraise, ) @staticmethod def _waiter_takes_last_result(waiter): if not six.callable(waiter): return False if isinstance(waiter, _wait.wait_base): waiter = waiter.__call__ waiter_spec = inspect.getargspec(waiter) return 'last_result' in waiter_spec.args def __repr__(self): attrs = dict( _utils.visible_attrs(self, attrs={'me': id(self)}), __class__=self.__class__.__name__, ) return ("<%(__class__)s object at 0x%(me)x (stop=%(stop)s, " "wait=%(wait)s, sleep=%(sleep)s, retry=%(retry)s, " "before=%(before)s, after=%(after)s)>") % (attrs) @property def statistics(self): """Return a dictionary of runtime statistics. This dictionary will be empty when the controller has never been ran. When it is running or has ran previously it should have (but may not) have useful and/or informational keys and values when running is underway and/or completed. .. warning:: The keys in this dictionary **should** be some what stable (not changing), but there existence **may** change between major releases as new statistics are gathered or removed so before accessing keys ensure that they actually exist and handle when they do not. .. note:: The values in this dictionary are local to the thread running call (so if multiple threads share the same retrying object - either directly or indirectly) they will each have there own view of statistics they have collected (in the future we may provide a way to aggregate the various statistics from each thread). """ try: return self._local.statistics except AttributeError: self._local.statistics = {} return self._local.statistics def wraps(self, f): """Wrap a function for retrying. :param f: A function to wraps for retrying. """ @six.wraps(f) def wrapped_f(*args, **kw): return self.call(f, *args, **kw) def retry_with(*args, **kwargs): return self.copy(*args, **kwargs).wraps(f) wrapped_f.retry = self wrapped_f.retry_with = retry_with return wrapped_f def begin(self, fn): self.fn = fn self.statistics.clear() self.statistics['start_time'] = now() self.statistics['attempt_number'] = 1 self.statistics['idle_for'] = 0 def iter(self, result, exc_info, start_time): fut = Future(self.statistics['attempt_number']) if result is not NO_RESULT: trial_end_time = now() fut.set_result(result) retry = self.retry(fut) elif exc_info: trial_end_time = now() t, e, tb = exc_info _utils.capture(fut, exc_info) if isinstance(e, TryAgain): retry = True else: retry = self.retry(fut) else: if self.before is not None: self.before(self.fn, self.statistics['attempt_number']) return DoAttempt() if not retry: return fut.result() if self.after is not None: trial_time_taken = trial_end_time - start_time self.after(self.fn, self.statistics['attempt_number'], trial_time_taken) delay_since_first_attempt = now() - self.statistics['start_time'] self.statistics['delay_since_first_attempt'] = \ delay_since_first_attempt if self.stop(self.statistics['attempt_number'], delay_since_first_attempt): if self.reraise: raise RetryError(fut).reraise() six.raise_from(RetryError(fut), fut.exception()) if self.wait: if self._wait_takes_result: sleep = self.wait(self.statistics['attempt_number'], delay_since_first_attempt, last_result=fut) else: sleep = self.wait(self.statistics['attempt_number'], delay_since_first_attempt) else: sleep = 0 self.statistics['idle_for'] += sleep self.statistics['attempt_number'] += 1 return DoSleep(sleep) class Retrying(BaseRetrying): """Retrying controller.""" def call(self, fn, *args, **kwargs): self.begin(fn) result = NO_RESULT exc_info = None start_time = now() while True: do = self.iter(result=result, exc_info=exc_info, start_time=start_time) if isinstance(do, DoAttempt): try: result = fn(*args, **kwargs) continue except Exception: exc_info = sys.exc_info() continue elif isinstance(do, DoSleep): result = NO_RESULT exc_info = None self.sleep(do) else: return do __call__ = call class Future(futures.Future): """Encapsulates a (future or past) attempted call to a target function.""" def __init__(self, attempt_number): super(Future, self).__init__() self.attempt_number = attempt_number @property def failed(self): """Return whether a exception is being held in this future.""" return self.exception() is not None @classmethod def construct(cls, attempt_number, value, has_exception): """Construct a new Future object.""" fut = cls(attempt_number) if has_exception: fut.set_exception(value) else: fut.set_result(value) return fut class RetryError(Exception): """Encapsulates the last attempt instance right before giving up.""" def __init__(self, last_attempt): self.last_attempt = last_attempt def reraise(self): if self.last_attempt.failed: raise self.last_attempt.result() raise self def __str__(self): return "RetryError[{0}]".format(self.last_attempt) if asyncio: from tenacity.async import AsyncRetrying if tornado: from tenacity.tornadoweb import TornadoRetrying tenacity-4.8.0/tenacity/_utils.py0000644000372000037200000000542513214435755017713 0ustar travistravis00000000000000# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect import sys import six # sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint... try: MAX_WAIT = sys.maxint / 2 except AttributeError: MAX_WAIT = 1073741823 if six.PY2: def capture(fut, tb): # TODO(harlowja): delete this in future, since its # has to repeatedly calculate this crap. fut.set_exception_info(tb[1], tb[2]) else: def capture(fut, tb): fut.set_exception(tb[1]) def visible_attrs(obj, attrs=None): if attrs is None: attrs = {} for attr_name, attr in inspect.getmembers(obj): if attr_name.startswith("_"): continue attrs[attr_name] = attr return attrs def find_ordinal(pos_num): # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers if pos_num == 0: return "th" elif pos_num == 1: return 'st' elif pos_num == 2: return 'nd' elif pos_num == 3: return 'rd' elif pos_num >= 4 and pos_num <= 20: return 'th' else: return find_ordinal(pos_num % 10) def to_ordinal(pos_num): return "%i%s" % (pos_num, find_ordinal(pos_num)) def get_callback_name(cb): """Get a callback fully-qualified name. If no name can be produced ``repr(cb)`` is called and returned. """ segments = [] try: segments.append(cb.__qualname__) except AttributeError: try: segments.append(cb.__name__) if inspect.ismethod(cb): try: # This attribute doesn't exist on py3.x or newer, so # we optionally ignore it... (on those versions of # python `__qualname__` should have been found anyway). segments.insert(0, cb.im_class.__name__) except AttributeError: pass except AttributeError: pass if not segments: return repr(cb) else: try: # When running under sphinx it appears this can be none? if cb.__module__: segments.insert(0, cb.__module__) except AttributeError: pass return ".".join(segments) tenacity-4.8.0/tenacity/after.py0000644000372000037200000000236213214435755017512 0ustar travistravis00000000000000# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tenacity import _utils def after_nothing(func, trial_number, trial_time_taken): """After call strategy that does nothing.""" def after_log(logger, log_level, sec_format="%0.3f"): """After call strategy that logs to some logger the finished attempt.""" log_tpl = ("Finished call to '%s' after " + str(sec_format) + "(s), " "this was the %s time calling it.") def log_it(func, trial_number, trial_time_taken): logger.log(log_level, log_tpl, _utils.get_callback_name(func), trial_time_taken, _utils.to_ordinal(trial_number)) return log_it tenacity-4.8.0/tenacity/async.py0000644000372000037200000000360513214435755017527 0ustar travistravis00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import sys from monotonic import monotonic as now from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import NO_RESULT class AsyncRetrying(BaseRetrying): def __init__(self, sleep=asyncio.sleep, **kwargs): super(AsyncRetrying, self).__init__(**kwargs) self.sleep = sleep @asyncio.coroutine def call(self, fn, *args, **kwargs): self.begin(fn) result = NO_RESULT exc_info = None start_time = now() while True: do = self.iter(result=result, exc_info=exc_info, start_time=start_time) if isinstance(do, DoAttempt): try: result = yield from fn(*args, **kwargs) exc_info = None continue except Exception: result = NO_RESULT exc_info = sys.exc_info() continue elif isinstance(do, DoSleep): result = NO_RESULT exc_info = None yield from self.sleep(do) else: return do tenacity-4.8.0/tenacity/before.py0000644000372000037200000000215113214435755017647 0ustar travistravis00000000000000# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tenacity import _utils def before_nothing(func, trial_number): """Before call strategy that does nothing.""" def before_log(logger, log_level): """Before call strategy that logs to some logger the attempt.""" def log_it(func, trial_number): logger.log(log_level, "Starting call to '%s', this is the %s time calling it.", _utils.get_callback_name(func), _utils.to_ordinal(trial_number)) return log_it tenacity-4.8.0/tenacity/nap.py0000644000372000037200000000213613214435755017166 0ustar travistravis00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import time #: Default sleep strategy. sleep = time.sleep class sleep_using_event(object): """Sleep strategy that waits on an event to be set.""" def __init__(self, event): self.event = event def __call__(self, timeout): # NOTE(harlowja): this may *not* actually wait for timeout # seconds if the event is set (ie this may eject out early). self.event.wait(timeout=timeout) tenacity-4.8.0/tenacity/retry.py0000644000372000037200000000676113214435755017565 0ustar travistravis00000000000000# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import six @six.add_metaclass(abc.ABCMeta) class retry_base(object): """Abstract base class for retry strategies.""" @abc.abstractmethod def __call__(self, attempt): pass def __and__(self, other): return retry_all(self, other) def __or__(self, other): return retry_any(self, other) class _retry_never(retry_base): """Retry strategy that never rejects any result.""" def __call__(self, attempt): return False retry_never = _retry_never() class _retry_always(retry_base): """Retry strategy that always rejects any result.""" def __call__(self, attempt): return True retry_always = _retry_always() class retry_if_exception(retry_base): """Retry strategy that retries if an exception verifies a predicate.""" def __init__(self, predicate): self.predicate = predicate def __call__(self, attempt): if attempt.failed: return self.predicate(attempt.exception()) class retry_if_exception_type(retry_if_exception): """Retries if an exception has been raised of one or more types.""" def __init__(self, exception_types=Exception): self.exception_types = exception_types super(retry_if_exception_type, self).__init__( lambda e: isinstance(e, exception_types)) class retry_unless_exception_type(retry_if_exception): """Retries until an exception is raised of one or more types.""" def __init__(self, exception_types=Exception): self.exception_types = exception_types super(retry_unless_exception_type, self).__init__( lambda e: not isinstance(e, exception_types)) def __call__(self, attempt): # always retry if no exception was raised if not attempt.failed: return True return self.predicate(attempt.exception()) class retry_if_result(retry_base): """Retries if the result verifies a predicate.""" def __init__(self, predicate): self.predicate = predicate def __call__(self, attempt): if not attempt.failed: return self.predicate(attempt.result()) class retry_if_not_result(retry_base): """Retries if the result refutes a predicate.""" def __init__(self, predicate): self.predicate = predicate def __call__(self, attempt): if not attempt.failed: return not self.predicate(attempt.result()) class retry_any(retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries): self.retries = retries def __call__(self, attempt): return any(map(lambda x: x(attempt), self.retries)) class retry_all(retry_base): """Retries if all the retries condition are valid.""" def __init__(self, *retries): self.retries = retries def __call__(self, attempt): return all(map(lambda x: x(attempt), self.retries)) tenacity-4.8.0/tenacity/stop.py0000644000372000037200000000534513214435755017402 0ustar travistravis00000000000000# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import six @six.add_metaclass(abc.ABCMeta) class stop_base(object): """Abstract base class for stop strategies.""" @abc.abstractmethod def __call__(self, previous_attempt_number, delay_since_first_attempt): pass def __and__(self, other): return stop_all(self, other) def __or__(self, other): return stop_any(self, other) class stop_any(stop_base): """Stop if any of the stop condition is valid.""" def __init__(self, *stops): self.stops = stops def __call__(self, previous_attempt_number, delay_since_first_attempt): return any(map( lambda x: x(previous_attempt_number, delay_since_first_attempt), self.stops)) class stop_all(stop_base): """Stop if all the stop conditions are valid.""" def __init__(self, *stops): self.stops = stops def __call__(self, previous_attempt_number, delay_since_first_attempt): return all(map( lambda x: x(previous_attempt_number, delay_since_first_attempt), self.stops)) class _stop_never(stop_base): """Never stop.""" def __call__(self, previous_attempt_number, delay_since_first_attempt): return False stop_never = _stop_never() class stop_when_event_set(stop_base): """Stop when the given event is set.""" def __init__(self, event): self.event = event def __call__(self, previous_attempt_number, delay_since_first_attempt): return self.event.is_set() class stop_after_attempt(stop_base): """Stop when the previous attempt >= max_attempt.""" def __init__(self, max_attempt_number): self.max_attempt_number = max_attempt_number def __call__(self, previous_attempt_number, delay_since_first_attempt): return previous_attempt_number >= self.max_attempt_number class stop_after_delay(stop_base): """Stop when the time from the first attempt >= limit.""" def __init__(self, max_delay): self.max_delay = max_delay def __call__(self, previous_attempt_number, delay_since_first_attempt): return delay_since_first_attempt >= self.max_delay tenacity-4.8.0/tenacity/tornadoweb.py0000644000372000037200000000345213214435755020556 0ustar travistravis00000000000000# -*- coding: utf-8 -*- # Copyright 2017 Elisey Zanko # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from monotonic import monotonic as now from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import NO_RESULT from tornado import gen class TornadoRetrying(BaseRetrying): def __init__(self, sleep=gen.sleep, **kwargs): super(TornadoRetrying, self).__init__(**kwargs) self.sleep = sleep @gen.coroutine def call(self, fn, *args, **kwargs): self.begin(fn) result = NO_RESULT exc_info = None start_time = now() while True: do = self.iter(result=result, exc_info=exc_info, start_time=start_time) if isinstance(do, DoAttempt): try: result = yield fn(*args, **kwargs) exc_info = None continue except Exception: result = NO_RESULT exc_info = sys.exc_info() continue elif isinstance(do, DoSleep): result = NO_RESULT exc_info = None yield self.sleep(do) else: raise gen.Return(do) tenacity-4.8.0/tenacity/wait.py0000644000372000037200000001467313214435755017365 0ustar travistravis00000000000000# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import random import six from tenacity import _utils @six.add_metaclass(abc.ABCMeta) class wait_base(object): """Abstract base class for wait strategies.""" @abc.abstractmethod def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): pass def __add__(self, other): return wait_combine(self, other) def __radd__(self, other): # make it possible to use multiple waits with the built-in sum function if other == 0: return self return self.__add__(other) class wait_fixed(wait_base): """Wait strategy that waits a fixed amount of time between each retry.""" def __init__(self, wait): self.wait_fixed = wait def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): return self.wait_fixed class wait_none(wait_fixed): """Wait strategy that doesn't wait at all before retrying.""" def __init__(self): super(wait_none, self).__init__(0) class wait_random(wait_base): """Wait strategy that waits a random amount of time between min/max.""" def __init__(self, min=0, max=1): # noqa self.wait_random_min = min self.wait_random_max = max def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): return (self.wait_random_min + (random.random() * (self.wait_random_max - self.wait_random_min))) class wait_combine(wait_base): """Combine several waiting strategies.""" def __init__(self, *strategies): self.wait_funcs = strategies def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): return sum(map( lambda x: x(previous_attempt_number, delay_since_first_attempt), self.wait_funcs)) class wait_chain(wait_base): """Chain two or more waiting strategies. If all strategies are exhausted, the very last strategy is used thereafter. For example:: @retry(wait=wait_chain(*[wait_fixed(1) for i in range(3)] + [wait_fixed(2) for j in range(5)] + [wait_fixed(5) for k in range(4))) def wait_chained(): print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s thereafter.") """ def __init__(self, *strategies): self.strategies = list(strategies) def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): wait_func = self.strategies[0] if len(self.strategies) > 1: self.strategies.pop(0) return wait_func(previous_attempt_number, delay_since_first_attempt) class wait_incrementing(wait_base): """Wait an incremental amount of time after each attempt. Starting at a starting value and incrementing by a value for each attempt (and restricting the upper limit to some maximum value). """ def __init__(self, start=0, increment=100, max=_utils.MAX_WAIT): # noqa self.start = start self.increment = increment self.max = max def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): result = self.start + ( self.increment * (previous_attempt_number - 1) ) return max(0, min(result, self.max)) class wait_exponential(wait_base): """Wait strategy that applies exponential backoff. It allows for a customized multiplier and an ability to restrict the upper limit to some maximum value. The intervals are fixed (i.e. there is no jitter), so this strategy is suitable for balancing retries against latency when a required resource is unavailable for an unknown duration, but *not* suitable for resolving contention between multiple processes for a shared resource. Use wait_random_exponential for the latter case. """ def __init__(self, multiplier=1, max=_utils.MAX_WAIT, exp_base=2): # noqa self.multiplier = multiplier self.max = max self.exp_base = exp_base def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): try: exp = self.exp_base ** previous_attempt_number result = self.multiplier * exp except OverflowError: return self.max return max(0, min(result, self.max)) class wait_random_exponential(wait_exponential): """Random wait with exponentially widening window. An exponential backoff strategy used to mediate contention between multiple unco-ordinated processes for a shared resource in distributed systems. This is the sense in which "exponential backoff" is meant in e.g. Ethernet networking, and corresponds to the "Full Jitter" algorithm described in this blog post: https://www.awsarchitectureblog.com/2015/03/backoff.html Each retry occurs at a random time in a geometrically expanding interval. It allows for a custom multiplier and an ability to restrict the upper limit of the random interval to some maximum value. Example:: wait_random_exponential(multiplier=0.5, # initial window 0.5s max=60) # max 60s timeout When waiting for an unavailable resource to become available again, as opposed to trying to resolve contention for a shared resource, the wait_exponential strategy (which uses a fixed interval) may be preferable. """ def __call__(self, previous_attempt_number, delay_since_first_attempt, last_result=None): high = super(wait_random_exponential, self).__call__( previous_attempt_number, delay_since_first_attempt) return random.uniform(0, high) tenacity-4.8.0/tenacity.egg-info/0000755000372000037200000000000013214436031017512 5ustar travistravis00000000000000tenacity-4.8.0/tenacity.egg-info/PKG-INFO0000644000372000037200000002713613214436031020620 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: tenacity Version: 4.8.0 Summary: Retry code until it succeeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: julien@danjou.info License: UNKNOWN Description-Content-Type: UNKNOWN Description: Tenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://img.shields.io/travis/jd/tenacity.svg :target: https://travis-ci.org/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from `a fork of retrying `_. The simplest use case is retrying a flaky function whenever an `Exception` occurs until a value is returned. .. testcode:: import random from tenacity import retry @retry def do_something_unreliable(): if random.randint(0, 10) > 1: raise IOError("Broken sauce, everything is hosed!!!111one") else: return "Awesome sauce!" print(do_something_unreliable()) .. testoutput:: :hide: Awesome sauce! Features -------- - Generic Decorator API - Specify stop condition (i.e. limit by number of attempts) - Specify wait condition (i.e. exponential backoff sleeping between attempts) - Customize retrying on Exceptions - Customize retrying on expected returned result - Retry on coroutines Installation ------------ To install *tenacity*, simply: .. code-block:: bash $ pip install tenacity Examples ---------- .. testsetup:: * import logging from tenacity import * class MyException(Exception): pass As you saw above, the default behavior is to retry forever without waiting when an exception is raised. .. testcode:: @retry def never_give_up_never_surrender(): print("Retry forever ignoring Exceptions, don't wait between retries") raise Exception Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. .. testcode:: @retry(stop=stop_after_attempt(7)) def stop_after_7_attempts(): print("Stopping after 7 attempts") raise Exception We don't have all day, so let's set a boundary for how long we should be retrying stuff. .. testcode:: @retry(stop=stop_after_delay(10)) def stop_after_10_s(): print("Stopping after 10 seconds") raise Exception You can combine several stop conditions by using the `|` operator: .. testcode:: @retry(stop=(stop_after_delay(10) | stop_after_attempt(5))) def stop_after_10_s_or_5_retries(): print("Stopping after 10 seconds or 5 retries") raise Exception Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. .. testcode:: @retry(wait=wait_fixed(2)) def wait_2_s(): print("Wait 2 second between retries") raise Exception Some things perform best with a bit of randomness injected. .. testcode:: @retry(wait=wait_random(min=1, max=2)) def wait_random_1_to_2_s(): print("Randomly wait 1 to 2 seconds between retries") raise Exception Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_exponential(multiplier=1, max=10)) def wait_exponential_1(): print("Wait 2^x * 1 second between each retry, up to 10 seconds, then 10 seconds afterwards") raise Exception Then again, it's also hard to beat combining fixed waits and jitter (to help avoid thundering herds) when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_fixed(3) + wait_random(0, 2)) def wait_fixed_jitter(): print("Wait at least 3 seconds, and add up to 2 seconds of random delay") raise Exception When multiple processes are in contention for a shared resource, exponentially increasing jitter helps minimise collisions. .. testcode:: @retry(wait=wait_random_exponential(multiplier=1, max=60)) def wait_exponential_jitter(): print("Randomly wait up to 2^x * 1 seconds between each retry until the range reaches 60 seconds, then randomly up to 60 seconds afterwards") raise Exception Sometimes it's necessary to build a chain of backoffs. .. testcode:: @retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)])) def wait_fixed_chained(): print("Wait 3s for 3 attempts, 7s for the next 2 attempts and 9s for all attempts thereafter") raise Exception We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. .. testcode:: @retry(retry=retry_if_exception_type(IOError)) def might_io_error(): print("Retry forever with no wait if an IOError occurs, raise any other errors") raise Exception We can also use the result of the function to alter the behavior of retrying. .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=retry_if_result(is_none_p)) def might_return_none(): print("Retry with no wait if return value is None") We can also combine several conditions: .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type())) def might_return_none(): print("Retry forever ignoring Exceptions with no wait if return value is None") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. It's also possible to retry explicitly at any time by raising the `TryAgain` exception: .. testcode:: @retry def do_something(): result = something_else() if result == 23: raise TryAgain While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. testcode:: @retry(reraise=True, stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except MyException: # timed out retrying pass It's possible to execute an action before any attempt of calling the function by using the before callback function: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") In the same spirit, It's possible to execute after a call that failed: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") You can access the statistics about the retry made over a function by using the `retry` attribute attached to the function and its `statistics` attribute: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... You can change the arguments of a retry decorator as needed when calling it by using the `retry_with` function attached to the wrapped function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception.retry_with(stop=stop_after_attempt(4))() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Make the docs better (or more detailed, or more easier to read, or ...) .. _`the repository`: https://github.com/jd/tenacity Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Utilities tenacity-4.8.0/tenacity.egg-info/SOURCES.txt0000644000372000037200000000121613214436031021376 0ustar travistravis00000000000000.travis.yml AUTHORS ChangeLog LICENSE README.rst requirements.txt setup.cfg setup.py tox.ini doc/source/conf.py doc/source/index.rst tenacity/__init__.py tenacity/_utils.py tenacity/after.py tenacity/async.py tenacity/before.py tenacity/nap.py tenacity/retry.py tenacity/stop.py tenacity/tornadoweb.py tenacity/wait.py tenacity.egg-info/PKG-INFO tenacity.egg-info/SOURCES.txt tenacity.egg-info/dependency_links.txt tenacity.egg-info/not-zip-safe tenacity.egg-info/pbr.json tenacity.egg-info/requires.txt tenacity.egg-info/top_level.txt tenacity/tests/__init__.py tenacity/tests/test_async.py tenacity/tests/test_tenacity.py tenacity/tests/test_tornado.pytenacity-4.8.0/tenacity.egg-info/dependency_links.txt0000644000372000037200000000000113214436031023560 0ustar travistravis00000000000000 tenacity-4.8.0/tenacity.egg-info/not-zip-safe0000644000372000037200000000000113214436031021740 0ustar travistravis00000000000000 tenacity-4.8.0/tenacity.egg-info/pbr.json0000644000372000037200000000005613214436031021171 0ustar travistravis00000000000000{"git_version": "69cf4a7", "is_release": true}tenacity-4.8.0/tenacity.egg-info/requires.txt0000644000372000037200000000010313214436031022104 0ustar travistravis00000000000000six>=1.9.0 monotonic>=0.6 [:(python_version=='2.7')] futures>=3.0 tenacity-4.8.0/tenacity.egg-info/top_level.txt0000644000372000037200000000001113214436031022234 0ustar travistravis00000000000000tenacity tenacity-4.8.0/.travis.yml0000644000372000037200000000162613214435755016332 0ustar travistravis00000000000000language: python python: - 2.7 - 3.5 - 3.6 - pypy install: pip install tox tox-travis nose script: tox deploy: provider: pypi user: jd password: secure: suzaymA+xzruyHuPiIcPIzWAZviuhe5L00nlHL0tN/zXkfYvxgXAAChI8BSO5bJBgHKXKrBazBUtPel36F62dESION6v6z7JoJEKQixSQCexejwz2eE8RJ3f+D2MB1fqK4IZcbI3HMyyyMJtF149E+iHN4RguLNpgxl24QHYlsZQdeSbmybjrj+su7MOH9BdJAtA1zdOKdljI5CraOlJsRV+MRzleMby2BDmjTJSoPuFFWB2/Z3BvnAo0YyLsWGCEeO0bKnTqPA8GX/Poi3u4I76FaFqHPOfbNxRw+oSFtS09hOkNqt+ePZV9n5gqrg9yp3E1YvJIOoNuvUGKtY4TVEhHHeDdQ1XC/eV4171NhYn+WkcGsvRWTMEgtp5ZL6GvMLwYuoM8DGmfkPHjwtMy1r5ihyHYQwVl2wrxKuUUUsPb2EqfHK7UiGFmS7A0go4yfsUXkEDk1yYGN4KHcYJcfSjTrgXU0dbT8glUe3sJOwX9jeBz3H7e3/zQ89nR1Xm1WMEWnt/47OmqGDvpcxtYipKXFmU3A85KBBFb8yjQvwFx2SrlAa/7Gp3u4niWIQfnl52qOqbh7cAgftDxKqU4w3t4+5sC3VbDbkDMAJAbDgeS+mLc68yX+Q7Yfk9+aLZVKOFJG1SPrQFXDFEeoZ3qej/gAtjsx3YqwCrinfJXNA= on: tags: true distributions: "sdist bdist_wheel" tenacity-4.8.0/AUTHORS0000644000372000037200000000066313214436031015255 0ustar travistravis00000000000000Brian Williams Brian-Williams Elisey Zanko Joshua Harlow Julien Danjou Michael Evans Victor Yap William Silversmith Zane Bitter Étienne BERSAC (bersace) tenacity-4.8.0/ChangeLog0000644000372000037200000000433613214436031015760 0ustar travistravis00000000000000CHANGES ======= 4.8.0 ----- * Allow waiters to introspect last result 4.7.1 ----- * Missed top level import of stop\_when\_event\_set * Mention Tornado minimum version in README * Fix unless\_exception test names and add no input test 4.7.0 ----- * Add Tornado support * Fix pep8 4.6.1 ----- * Simplify wrapping code 4.6.0 ----- * Implement \`retry\_with' on decorated functions 4.5.0 ----- * Replace hacking with flake8 extensions * Fix import order * Put a default sleep attribute on BaseRetrying * async: allow to specify a different sleep method * Issue-8: Make sure doc examples run (#89) * Remove Python 3.4 support * Deploy tags with Travis 4.4.0 ----- * Document wait\_random\_exponential strategy * Rename wait\_full\_jitter to wait\_random\_exponential 4.3.0 ----- * Add retry\_unless\_exception\_type * Add missing ')' to README.rst * Do not duplicate statistics fields in instance * Do not start trial\_start\_time as an attribute * Add an example on how to combine stop conditions 4.2.0 ----- * Put a link to the GitHub issue of retrying about the fork * feat: added wait\_full\_jitter * Add new event based sleep and stop * Add \`wraps' method directly on Retrying object 4.1.0 ----- * wait: handle OverflowError * doc: raise Exception in example so they actually work * Remove weird example with retry\_if\_io\_error * Bump version for new feature * Add Python 3.6 support * Add retry\_if\_not\_result class to compliment the existing retry\_if\_result class 4.0.1 ----- * Fix six minimum version * doc: rewrite the before explanation 4.0.0 ----- * Add \_\_call\_\_ on BaseRetrying class * Document before and after keywords * Remove useless MANIFEST * Remove non-working PyPI download image * Bump hacking to 0.13 * Use Python 3 for pep8 tox target * Remove deprecated wait\_jitter 3.7.1 ----- * Fix pep8 errors 3.7.0 ----- * Correctly set the exception if we TryAgain for ever 3.6.0 ----- * Retry on coroutines * Run flake8 only with latest python * Deduplicate retry decorator logic * Extract controller IOs in subclass 3.5.0 ----- * Allow to combine stop conditions * Add SayThanks * retry: implement bitwise operators on retry strategies * retry: add retry\_all 3.4.0 ----- * Deprecate wait\_jitter for wait\_random tenacity-4.8.0/LICENSE0000644000372000037200000002613513214435755015230 0ustar travistravis00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.tenacity-4.8.0/README.rst0000644000372000037200000002071413214435755015707 0ustar travistravis00000000000000Tenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://img.shields.io/travis/jd/tenacity.svg :target: https://travis-ci.org/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from `a fork of retrying `_. The simplest use case is retrying a flaky function whenever an `Exception` occurs until a value is returned. .. testcode:: import random from tenacity import retry @retry def do_something_unreliable(): if random.randint(0, 10) > 1: raise IOError("Broken sauce, everything is hosed!!!111one") else: return "Awesome sauce!" print(do_something_unreliable()) .. testoutput:: :hide: Awesome sauce! Features -------- - Generic Decorator API - Specify stop condition (i.e. limit by number of attempts) - Specify wait condition (i.e. exponential backoff sleeping between attempts) - Customize retrying on Exceptions - Customize retrying on expected returned result - Retry on coroutines Installation ------------ To install *tenacity*, simply: .. code-block:: bash $ pip install tenacity Examples ---------- .. testsetup:: * import logging from tenacity import * class MyException(Exception): pass As you saw above, the default behavior is to retry forever without waiting when an exception is raised. .. testcode:: @retry def never_give_up_never_surrender(): print("Retry forever ignoring Exceptions, don't wait between retries") raise Exception Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. .. testcode:: @retry(stop=stop_after_attempt(7)) def stop_after_7_attempts(): print("Stopping after 7 attempts") raise Exception We don't have all day, so let's set a boundary for how long we should be retrying stuff. .. testcode:: @retry(stop=stop_after_delay(10)) def stop_after_10_s(): print("Stopping after 10 seconds") raise Exception You can combine several stop conditions by using the `|` operator: .. testcode:: @retry(stop=(stop_after_delay(10) | stop_after_attempt(5))) def stop_after_10_s_or_5_retries(): print("Stopping after 10 seconds or 5 retries") raise Exception Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. .. testcode:: @retry(wait=wait_fixed(2)) def wait_2_s(): print("Wait 2 second between retries") raise Exception Some things perform best with a bit of randomness injected. .. testcode:: @retry(wait=wait_random(min=1, max=2)) def wait_random_1_to_2_s(): print("Randomly wait 1 to 2 seconds between retries") raise Exception Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_exponential(multiplier=1, max=10)) def wait_exponential_1(): print("Wait 2^x * 1 second between each retry, up to 10 seconds, then 10 seconds afterwards") raise Exception Then again, it's also hard to beat combining fixed waits and jitter (to help avoid thundering herds) when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_fixed(3) + wait_random(0, 2)) def wait_fixed_jitter(): print("Wait at least 3 seconds, and add up to 2 seconds of random delay") raise Exception When multiple processes are in contention for a shared resource, exponentially increasing jitter helps minimise collisions. .. testcode:: @retry(wait=wait_random_exponential(multiplier=1, max=60)) def wait_exponential_jitter(): print("Randomly wait up to 2^x * 1 seconds between each retry until the range reaches 60 seconds, then randomly up to 60 seconds afterwards") raise Exception Sometimes it's necessary to build a chain of backoffs. .. testcode:: @retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)])) def wait_fixed_chained(): print("Wait 3s for 3 attempts, 7s for the next 2 attempts and 9s for all attempts thereafter") raise Exception We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. .. testcode:: @retry(retry=retry_if_exception_type(IOError)) def might_io_error(): print("Retry forever with no wait if an IOError occurs, raise any other errors") raise Exception We can also use the result of the function to alter the behavior of retrying. .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=retry_if_result(is_none_p)) def might_return_none(): print("Retry with no wait if return value is None") We can also combine several conditions: .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type())) def might_return_none(): print("Retry forever ignoring Exceptions with no wait if return value is None") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. It's also possible to retry explicitly at any time by raising the `TryAgain` exception: .. testcode:: @retry def do_something(): result = something_else() if result == 23: raise TryAgain While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. testcode:: @retry(reraise=True, stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except MyException: # timed out retrying pass It's possible to execute an action before any attempt of calling the function by using the before callback function: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") In the same spirit, It's possible to execute after a call that failed: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") You can access the statistics about the retry made over a function by using the `retry` attribute attached to the function and its `statistics` attribute: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... You can change the arguments of a retry decorator as needed when calling it by using the `retry_with` function attached to the wrapped function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception.retry_with(stop=stop_after_attempt(4))() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Make the docs better (or more detailed, or more easier to read, or ...) .. _`the repository`: https://github.com/jd/tenacity tenacity-4.8.0/requirements.txt0000644000372000037200000000011213214435755017472 0ustar travistravis00000000000000six>=1.9.0 futures>=3.0;python_version=='2.7' monotonic>=0.6 # Apache-2.0 tenacity-4.8.0/setup.cfg0000644000372000037200000000133013214436031016016 0ustar travistravis00000000000000[metadata] name = tenacity url = https://github.com/jd/tenacity summary = Retry code until it succeeeds description-file = README.rst author = Julien Danjou author-email = julien@danjou.info home-page = https://github.com/jd/tenacity classifier = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Topic :: Utilities [global] setup-hooks = pbr.hooks.setup_hook [files] packages = tenacity [wheel] universal = 1 [build_sphinx] source-dir = doc/source build-dir = doc/_build builder = doctest,html [egg_info] tag_build = tag_date = 0 tenacity-4.8.0/setup.py0000644000372000037200000000121113214435755015721 0ustar travistravis00000000000000#!/usr/bin/env python # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr'], pbr=True) tenacity-4.8.0/tox.ini0000644000372000037200000000124313214435755015527 0ustar travistravis00000000000000[tox] envlist = py27,py34,py35,py36,pep8,pypy [tox:travis] 2.7 = py27 3.5 = py35 3.6 = pep8,py36 pypy = pypy [testenv] usedevelop = True sitepackages = False deps = nose sphinx tornado commands = py{27,py}: python setup.py nosetests --ignore-files '.*async.py' py3{5,6}: python setup.py nosetests python setup.py build_sphinx [testenv:pep8] basepython = python3 deps = flake8 flake8-import-order flake8-blind-except flake8-builtins flake8-docstrings flake8-rst-docstrings flake8-logging-format commands = flake8 [flake8] exclude = .tox,.eggs show-source = true ignore = D100,D101,D102,D103,D104,D105,D107 tenacity-4.8.0/PKG-INFO0000644000372000037200000002713613214436031015306 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: tenacity Version: 4.8.0 Summary: Retry code until it succeeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: julien@danjou.info License: UNKNOWN Description-Content-Type: UNKNOWN Description: Tenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://img.shields.io/travis/jd/tenacity.svg :target: https://travis-ci.org/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from `a fork of retrying `_. The simplest use case is retrying a flaky function whenever an `Exception` occurs until a value is returned. .. testcode:: import random from tenacity import retry @retry def do_something_unreliable(): if random.randint(0, 10) > 1: raise IOError("Broken sauce, everything is hosed!!!111one") else: return "Awesome sauce!" print(do_something_unreliable()) .. testoutput:: :hide: Awesome sauce! Features -------- - Generic Decorator API - Specify stop condition (i.e. limit by number of attempts) - Specify wait condition (i.e. exponential backoff sleeping between attempts) - Customize retrying on Exceptions - Customize retrying on expected returned result - Retry on coroutines Installation ------------ To install *tenacity*, simply: .. code-block:: bash $ pip install tenacity Examples ---------- .. testsetup:: * import logging from tenacity import * class MyException(Exception): pass As you saw above, the default behavior is to retry forever without waiting when an exception is raised. .. testcode:: @retry def never_give_up_never_surrender(): print("Retry forever ignoring Exceptions, don't wait between retries") raise Exception Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. .. testcode:: @retry(stop=stop_after_attempt(7)) def stop_after_7_attempts(): print("Stopping after 7 attempts") raise Exception We don't have all day, so let's set a boundary for how long we should be retrying stuff. .. testcode:: @retry(stop=stop_after_delay(10)) def stop_after_10_s(): print("Stopping after 10 seconds") raise Exception You can combine several stop conditions by using the `|` operator: .. testcode:: @retry(stop=(stop_after_delay(10) | stop_after_attempt(5))) def stop_after_10_s_or_5_retries(): print("Stopping after 10 seconds or 5 retries") raise Exception Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. .. testcode:: @retry(wait=wait_fixed(2)) def wait_2_s(): print("Wait 2 second between retries") raise Exception Some things perform best with a bit of randomness injected. .. testcode:: @retry(wait=wait_random(min=1, max=2)) def wait_random_1_to_2_s(): print("Randomly wait 1 to 2 seconds between retries") raise Exception Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_exponential(multiplier=1, max=10)) def wait_exponential_1(): print("Wait 2^x * 1 second between each retry, up to 10 seconds, then 10 seconds afterwards") raise Exception Then again, it's also hard to beat combining fixed waits and jitter (to help avoid thundering herds) when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_fixed(3) + wait_random(0, 2)) def wait_fixed_jitter(): print("Wait at least 3 seconds, and add up to 2 seconds of random delay") raise Exception When multiple processes are in contention for a shared resource, exponentially increasing jitter helps minimise collisions. .. testcode:: @retry(wait=wait_random_exponential(multiplier=1, max=60)) def wait_exponential_jitter(): print("Randomly wait up to 2^x * 1 seconds between each retry until the range reaches 60 seconds, then randomly up to 60 seconds afterwards") raise Exception Sometimes it's necessary to build a chain of backoffs. .. testcode:: @retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)])) def wait_fixed_chained(): print("Wait 3s for 3 attempts, 7s for the next 2 attempts and 9s for all attempts thereafter") raise Exception We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. .. testcode:: @retry(retry=retry_if_exception_type(IOError)) def might_io_error(): print("Retry forever with no wait if an IOError occurs, raise any other errors") raise Exception We can also use the result of the function to alter the behavior of retrying. .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=retry_if_result(is_none_p)) def might_return_none(): print("Retry with no wait if return value is None") We can also combine several conditions: .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type())) def might_return_none(): print("Retry forever ignoring Exceptions with no wait if return value is None") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. It's also possible to retry explicitly at any time by raising the `TryAgain` exception: .. testcode:: @retry def do_something(): result = something_else() if result == 23: raise TryAgain While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. testcode:: @retry(reraise=True, stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except MyException: # timed out retrying pass It's possible to execute an action before any attempt of calling the function by using the before callback function: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") In the same spirit, It's possible to execute after a call that failed: .. testcode:: logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") You can access the statistics about the retry made over a function by using the `retry` attribute attached to the function and its `statistics` attribute: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... You can change the arguments of a retry decorator as needed when calling it by using the `retry_with` function attached to the wrapped function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception.retry_with(stop=stop_after_attempt(4))() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Make the docs better (or more detailed, or more easier to read, or ...) .. _`the repository`: https://github.com/jd/tenacity Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Utilities