diff --git a/Pipfile b/Pipfile index 8d35903..4dfeac5 100644 --- a/Pipfile +++ b/Pipfile @@ -36,6 +36,8 @@ faker = "==14.1.0" # A Python module to parse, validate and reformat standard numbers and codes in different formats. # It contains a large collection of number formats. https://pypi.org/project/python-stdnum/ python-stdnum = "==1.17" +# Retrying library for Python. https://pypi.org/project/tenacity/ +tenacity = "==8.0.1" [dev-packages] # Black is the uncompromising Python code formatter. By using it, you agree to cede control over minutiae of diff --git a/Pipfile.lock b/Pipfile.lock index 1c19cf0..ff1f82b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "498e0113dec145196784d60c5ee8d7b1b6cb954230420a07b1a9c9665b67753b" + "sha256": "766776e54076f30541b64685f5fae8647381d52d991a22dcd131e2726c4c78f6" }, "pipfile-spec": 6, "requires": { @@ -216,6 +216,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, + "tenacity": { + "hashes": [ + "sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f", + "sha256:f78f4ea81b0fabc06728c11dc2a8c01277bfc5181b321a4770471902e3eb844a" + ], + "index": "pypi", + "version": "==8.0.1" + }, "win32-setctime": { "hashes": [ "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", @@ -461,10 +469,11 @@ }, "pathspec": { "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", + "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" ], - "version": "==0.9.0" + "markers": "python_version >= '3.7'", + "version": "==0.10.1" }, "platformdirs": { "hashes": [ @@ -675,11 +684,11 @@ }, "virtualenv": { "hashes": [ - "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1", - "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9" + "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782", + "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22" ], "markers": "python_version >= '3.6'", - "version": "==20.16.3" + "version": "==20.16.4" } } } diff --git a/python_boilerplate/demo/__init__.py b/python_boilerplate/demo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python_boilerplate/demo/tenacity_usage.py b/python_boilerplate/demo/tenacity_usage.py new file mode 100644 index 0000000..cdea8e1 --- /dev/null +++ b/python_boilerplate/demo/tenacity_usage.py @@ -0,0 +1,71 @@ +import logging +import random +from logging import INFO, WARNING +from typing import Final + +from loguru import logger +from tenacity import ( + after_log, + retry, + retry_if_exception_type, + retry_if_result, + stop_after_attempt, + wait_fixed, +) + +SUCCESS_RANGE: Final = range(200, 300) +loging_logger: Final = logging.getLogger(__name__) + + +# https://tenacity.readthedocs.io/en/latest/ +# https://jamesfheath.com/2020/07/python-library-tenacity.html + + +@retry(stop=stop_after_attempt(3), after=after_log(loging_logger, WARNING)) +def exception_function_1() -> None: + logger.warning("Mocking failure 1") + raise RuntimeError("Failure message 1") + + +@retry( + stop=stop_after_attempt(3), wait=wait_fixed(2), after=after_log(loging_logger, INFO) +) +def exception_function_2() -> None: + logger.warning("Mocking failure 2") + raise RuntimeError("Failure message 2") + + +@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type((IOError, ValueError))) +def different_exceptions_possible(x: int) -> str: + if x == 1: + logger.error("IO Error because x is 1") + raise IOError + elif x == 2: + logger.error("Connection Error because x is 2") + raise ConnectionError + elif x == 3: + logger.error("Type Error because x is 3") + raise TypeError + else: + return "success" + + +def validate_code(result: int) -> bool: + needs_retry = result not in SUCCESS_RANGE + if needs_retry: + logger.warning( + f"The result = {result}, which is NOT 2xx code, needs_retry = {needs_retry}" + ) + else: + logger.info( + f"The result = {result}, which is 2xx code, needs_retry = {needs_retry}" + ) + return needs_retry + + +@retry(stop=stop_after_attempt(3), retry=retry_if_result(validate_code)) +def customized_retry_logic_function(input_int: int) -> int: + random_int = random.randint(0, 200) + result = input_int + random_int + logger.info(f"input_int + random_int = {input_int} + {random_int} = {result}") + return result diff --git a/tests/demo/__init__.py b/tests/demo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/demo/test_tenacity_usage.py b/tests/demo/test_tenacity_usage.py new file mode 100644 index 0000000..a12b412 --- /dev/null +++ b/tests/demo/test_tenacity_usage.py @@ -0,0 +1,36 @@ +import pytest +from tenacity import RetryError + +from python_boilerplate.demo.tenacity_usage import ( + customized_retry_logic_function, + different_exceptions_possible, + exception_function_1, + exception_function_2, +) + + +def test_exception_function_1(): + with pytest.raises(RetryError): + exception_function_1() + + +def test_exception_function_2(): + with pytest.raises(RetryError): + exception_function_2() + + +def test_different_exceptions_possible(): + with pytest.raises(RetryError): + different_exceptions_possible(1) + with pytest.raises(RetryError): + different_exceptions_possible(2) + with pytest.raises(TypeError): + different_exceptions_possible(3) + assert different_exceptions_possible(4) == "success" + + +def test_customized_retry_logic_function(): + try: + customized_retry_logic_function(240) + except Exception as ex: + assert True, f"Test passed even if exception raised, {ex.__str__()}"