Package: tubthumper
Tubthumper: Helping you get up ... again!What's in a name?Tubthumperis a Python package of retry utilities named after the English anarcho-communist rock band Chumbawamba's 1997 hitTubthumping. Yes, really.I get knocked down, but I get up again. 🎶You're never gonna keep me down. 🎶I get knocked down, but I get up again. 🎶You're never gonna keep me down... 🎶Getting StartedInstallationtubthumperis a pip-installable packagehosted on PyPI. Getting started is as easy as:$pipinstalltubthumpertubthumperrequires Python 3.8 or greater. For Python 3.10 or greater, it has no external dependencies, i.e. standard library only, but earlier versions requiretyping-extensions.UsageImporttubthumper's useful bits:>>>fromtubthumperimportretry,retry_decorator,retry_factoryCall a function with retry and jittered exponential backoff:>>>retry(get_ip,exceptions=ConnectionError)WARNING:Functionthrewexceptionbelowontry1,retryingin0.844422secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}Call that same function with positional and keyword arguments, e.g. retryget_ip(42, "test", dev=True):>>>retry(get_ip,...args=(42,"test"),kwargs={"dev":True},...exceptions=ConnectionError)WARNING:Functionthrewexceptionbelowontry1,retryingin0.420572secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}Bake retry behavior into your function with a decorator:>>>@retry_decorator(exceptions=ConnectionError)...defget_ip_retry():...returnrequests.get("http://ip.jsontest.com").json()>>>get_ip_retry()WARNING:Functionthrewexceptionbelowontry1,retryingin0.511275secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}Create a new function with retry behavior from an existing one:>>>get_ip_retry=retry_factory(get_ip,exceptions=ConnectionError)>>>get_ip_retry()WARNING:Functionthrewexceptionbelowontry1,retryingin0.783799secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}CustomizationWhiletubthumperships with a set of sensible defaults, its retry behavior is fully customizable.ExceptionsBecause overbroad except clauses arethe most diabolical Python antipattern, there is no sensible default for what exception or exceptions to catch and retry. Thus, everytubthumperinterface has a requiredexceptionskeyword-only argument, which takes an exception or tuple of exceptions to catch and retry on, i.e. a sensible lack of a default.>>>retry(get_ip,exceptions=ConnectionError)WARNING:Functionthrewexceptionbelowontry1,retryingin0.476597secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}>>>retry(get_ip,exceptions=(KeyError,ConnectionError))WARNING:Functionthrewexceptionbelowontry1,retryingin0.908113secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}By default,tubthumperraises atubthumper.RetryErrorexception when all retries have been exhausted:>>>retry(lambda:1/0,retry_limit=0,exceptions=ZeroDivisionError)Traceback(mostrecentcalllast):...tubthumper._retry_factory.RetryError:Retrylimit0reachedYou can override this behavior using thereraiseflag to reraise the original exception in place ofRetryError:>>>retry(lambda:1/0,retry_limit=0,reraise=True,exceptions=ZeroDivisionError)Traceback(mostrecentcalllast):...ZeroDivisionError:divisionbyzeroRetry LimitsBy default,tubthumperwill retry endlessly, but you have two means of limiting retry behavior. As shown previously, to limit the number of retries attempted, use theretry_limitkeyword-only argument:>>>retry(lambda:1/0,retry_limit=10,exceptions=ZeroDivisionError)...# Warning logs for each failed callTraceback(mostrecentcalllast):...tubthumper._retry_factory.RetryError:Retrylimit10reachedAlternatively, you can use thetime_limitkeyword-only argument to prevent retry attempts after a certain duration:>>>retry(lambda:1/0,time_limit=60,exceptions=ZeroDivisionError)...# Warning logs for each failed callTraceback(mostrecentcalllast):...tubthumper._retry_factory.RetryError:Timelimit60exceededBackoff timingBy default, the backoff duration doubles with each retry, starting off at one second. As well, each backoff period is jittered, i.e. scaled by a uniformly distributed random number on the [0.0, 1.0) interval. You can disable jittering using thejitterkeyword-only argument:>>>retry(get_ip,jitter=False,exceptions=ConnectionError)WARNING:Functionthrewexceptionbelowontry1,retryingin1secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}You can set the initial backoff duration using theinit_backoffkeyword-only argument:>>>retry(get_ip,jitter=False,init_backoff=10,exceptions=ConnectionError)WARNING:Functionthrewexceptionbelowontry1,retryingin10secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}Finally, you can set the factor by which each successive backoff duration is scaled using theexponentialkeyword-only argument:>>>retry(get_ip,jitter=False,exponential=3,exceptions=ConnectionError)WARNING:Functionthrewexceptionbelowontry1,retryingin1secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.comWARNING:Functionthrewexceptionbelowontry2,retryingin3secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}LoggingBy default,tubthumperlogs each caught exception at thelogging.WARNINGlevel using a logger namedtubthumper, i.e.logging.getLogger("tubthumper"). As described in thePython logging tutorial, for this default logger, "events of severity WARNING and greater will be printed to sys.stderr" if no further logging is configured.You can set the logging level using thelog_levelkeyword-only argument:>>>retry(get_ip,log_level=logging.DEBUG,exceptions=ConnectionError)# No warnings{'ip':'8.8.8.8'}You can provide your own logger using theloggerkeyword-only argument. This logger'slogmethod will be called like so:logger.log(log_level,"Function threw...",exc_info=True)FeaturesCompatible with methodstubthumper's various interfaces are compatible with methods, including classmethods and staticmethods:>>>classClass:...@retry_decorator(exceptions=ConnectionError)...defget_ip(self):...returnrequests.get("http://ip.jsontest.com").json()...>>>Class().get_ip()WARNING:Functionthrewexceptionbelowontry1,retryingin0.719705secondsTraceback(mostrecentcalllast):...requests.exceptions.ConnectionError:http://ip.jsontest.com{'ip':'8.8.8.8'}Signature preservingtubthumper's various interfaces preserve the relevantdunderattributes of your function:>>>@retry_decorator(exceptions=ConnectionError)...deffunc(one:bool,two:float=3.0)->complex:..."""This is a docstring"""...>>>func.__name__'func'>>>func.__qualname__'func'>>>func.__module__'__main__'>>>func.__doc__'This is a docstring'>>>func.__annotations__{'one':<class'bool'>, 'two': <class 'float'>, 'return': <class 'complex'>}tubthumperalso preserves the inspect module's function signature, andis*functions:>>>importinspect>>>inspect.signature(func)<Signature(one:bool,two:float=3.0)->complex>>>>inspect.isfunction(func)True>>>inspect.isroutine(func)True>>>inspect.ismethod(Class().get_ip)TrueAsync supporttubthumper's various interfaces support coroutine functions, includinggenerator-based coroutines, awaiting them while usingasync.sleepbetween awaits:>>>@retry_decorator(exceptions=ConnectionError)...asyncdefget_ip():...returnrequests.get("http://ip.jsontest.com").json()...>>>inspect.iscoroutinefunction(get_ip)TrueFully type annotatedtubthumper's various interfaces are fully type annotated, passingMypy's static type checker. You can find Mypy'sType Check Coverage Summaryat that link.100% Test Coveragetubthumperachieves 100% test coverage across three supported operating systems (Windows, MacOS, & Linux). You can find theLinux coverage reportat that link, or the full coverage report onCodecov.