From 79bb02929fcb711d2b457843e216fe6d8423c2b9 Mon Sep 17 00:00:00 2001 From: Dravis-Xam Date: Wed, 12 Nov 2025 12:31:40 +0300 Subject: [PATCH 01/12] Created a yml file for auto-generating pystubs on workflow --- .github/workflows/generate-pystubs.yml | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/generate-pystubs.yml diff --git a/.github/workflows/generate-pystubs.yml b/.github/workflows/generate-pystubs.yml new file mode 100644 index 0000000..98f6715 --- /dev/null +++ b/.github/workflows/generate-pystubs.yml @@ -0,0 +1,41 @@ +name: Generate PyStubs on Merge + +on: + push: + branches: + - master # or 'main' if that’s your default branch + pull_request: + branches: [ master ] + +jobs: + generate-stubs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # needed for committing changes later + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + pip install mypy + + - name: Generate type stubs + run: | + # Adjust 'src' to your code folder + mkdir -p stubs + stubgen -p your_package_name -o stubs + + - name: Commit and push generated stubs + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add stubs/ + git commit -m "Auto-generate PyStubs on merge to master" || echo "No changes to commit" + git push From f0f8302d19ec7ff003b0ca57b3b6a1169233aaf2 Mon Sep 17 00:00:00 2001 From: Dravis-Xam Date: Wed, 12 Nov 2025 14:29:58 +0300 Subject: [PATCH 02/12] Added new feature for Retry Logic using tenacity --- .idea/.gitignore | 3 + .../inspectionProfiles/profiles_settings.xml | 6 ++ .idea/misc.xml | 7 +++ .idea/modules.xml | 8 +++ .idea/mpesakit.iml | 8 +++ .idea/vcs.xml | 6 ++ mpesakit/retry/__init__.py | 6 ++ mpesakit/retry/retry.py | 55 +++++++++++++++++++ pyproject.toml | 1 + tests/integration/retry/__init__.py | 0 tests/integration/retry/retry.py | 16 ++++++ 11 files changed, 116 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/mpesakit.iml create mode 100644 .idea/vcs.xml create mode 100644 mpesakit/retry/__init__.py create mode 100644 mpesakit/retry/retry.py create mode 100644 tests/integration/retry/__init__.py create mode 100644 tests/integration/retry/retry.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..355781e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e8d4f10 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/mpesakit.iml b/.idea/mpesakit.iml new file mode 100644 index 0000000..3672eb2 --- /dev/null +++ b/.idea/mpesakit.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/mpesakit/retry/__init__.py b/mpesakit/retry/__init__.py new file mode 100644 index 0000000..49f49ab --- /dev/null +++ b/mpesakit/retry/__init__.py @@ -0,0 +1,6 @@ +from retry import * + +__all__ = { + "retryable_request", + "post_request" +} \ No newline at end of file diff --git a/mpesakit/retry/retry.py b/mpesakit/retry/retry.py new file mode 100644 index 0000000..5f9d279 --- /dev/null +++ b/mpesakit/retry/retry.py @@ -0,0 +1,55 @@ +# mpesakit/retry.py + +""" +HOW IT WORKS: +____________ + +Decorator Factory: retryable_request() is reusable and configurable. + +Logging: Logs each retry attempt. + +Post request helper: post_request() wraps requests.post for automatic retries. +""" +from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +import requests +import logging + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def retryable_request( + stop_attempts: int = 3, + wait_min: int = 1, + wait_max: int = 10, + exception_type=Exception +): + """ + Decorator factory that returns a retry decorator using tenacity. + + Usage: + @retryable_request() + def your_func(...): + ... + """ + return retry( + stop=stop_after_attempt(stop_attempts), + wait=wait_exponential(multiplier=1, min=wait_min, max=wait_max), + retry=retry_if_exception_type(exception_type), + before=lambda retry_state: logger.info( + f"Retrying {retry_state.fn.__name__} after attempt {retry_state.attempt_number}" + ), + after=lambda retry_state: logger.warning( + f"Finished attempt {retry_state.attempt_number} for {retry_state.fn.__name__}" + ), + ) + + +# Example wrapper for requests.post +@retryable_request(exception_type=requests.exceptions.RequestException) +def post_request(url: str, payload: dict, headers: dict = None): + """Send POST request with retry logic.""" + headers = headers or {} + response = requests.post(url, json=payload, headers=headers, timeout=10) + response.raise_for_status() + return response.json() diff --git a/pyproject.toml b/pyproject.toml index e09e0b9..16e5e04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dependencies = [ "typing_extensions >= 4.12.2,<5.0.0", "cryptography >=41.0.7", "httpx >=0.27.0,<1.0.0", + "tenacity>=8.2.2" ] [project.urls] diff --git a/tests/integration/retry/__init__.py b/tests/integration/retry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/retry/retry.py b/tests/integration/retry/retry.py new file mode 100644 index 0000000..69e5c30 --- /dev/null +++ b/tests/integration/retry/retry.py @@ -0,0 +1,16 @@ +# tests/test_retry.py +import pytest +from mpesakit.retry.retry import retryable_request +import random + +# A dummy function to simulate failure +@retryable_request(stop_attempts=5, wait_min=0, wait_max=0, exception_type=ValueError) +def sometimes_fails(): + """Fails randomly to test retry logic.""" + if random.random() < 0.7: + raise ValueError("Random failure") + return "Success" + +def test_retryable_request(): + result = sometimes_fails() + assert result == "Success" From 542279b6729aa42fd361c34e8a40833bf69abc8a Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:36:07 +0300 Subject: [PATCH 03/12] Delete .github/workflows/generate-pystubs.yml --- .github/workflows/generate-pystubs.yml | 41 -------------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/generate-pystubs.yml diff --git a/.github/workflows/generate-pystubs.yml b/.github/workflows/generate-pystubs.yml deleted file mode 100644 index 98f6715..0000000 --- a/.github/workflows/generate-pystubs.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Generate PyStubs on Merge - -on: - push: - branches: - - master # or 'main' if that’s your default branch - pull_request: - branches: [ master ] - -jobs: - generate-stubs: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # needed for committing changes later - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - pip install mypy - - - name: Generate type stubs - run: | - # Adjust 'src' to your code folder - mkdir -p stubs - stubgen -p your_package_name -o stubs - - - name: Commit and push generated stubs - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add stubs/ - git commit -m "Auto-generate PyStubs on merge to master" || echo "No changes to commit" - git push From 9d1f91c8a54bd0be792c3f40713412cbd4e08b28 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:36:50 +0300 Subject: [PATCH 04/12] Delete .idea/.gitignore --- .idea/.gitignore | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml From b97a9a0c8f2e0cb2fa15ded2340e2fcb22548d7f Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:37:07 +0300 Subject: [PATCH 05/12] Delete .idea/inspectionProfiles/profiles_settings.xml --- .idea/inspectionProfiles/profiles_settings.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file From dc7d6320ca1b6bc9615292916f7a126545530c50 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:19:32 +0300 Subject: [PATCH 06/12] Delete .idea/misc.xml --- .idea/misc.xml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .idea/misc.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 355781e..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file From 0bd9bdc684d0dd6c48414f36fdff68bfaa56e6e9 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:19:53 +0300 Subject: [PATCH 07/12] Delete .idea/modules.xml --- .idea/modules.xml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .idea/modules.xml diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e8d4f10..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file From 71415a16f7dbdbce1cdb6b95e77747dfb2c8b134 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:20:08 +0300 Subject: [PATCH 08/12] Delete .idea/vcs.xml --- .idea/vcs.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 57009829739c34656dc81250e0c2fd1579c99156 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:20:33 +0300 Subject: [PATCH 09/12] Delete .idea/mpesakit.iml --- .idea/mpesakit.iml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .idea/mpesakit.iml diff --git a/.idea/mpesakit.iml b/.idea/mpesakit.iml deleted file mode 100644 index 3672eb2..0000000 --- a/.idea/mpesakit.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file From 00171453666c7aceec65fe2f07037cedc517ee22 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:22:24 +0300 Subject: [PATCH 10/12] Update __init__.py --- mpesakit/retry/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpesakit/retry/__init__.py b/mpesakit/retry/__init__.py index 49f49ab..b2668cd 100644 --- a/mpesakit/retry/__init__.py +++ b/mpesakit/retry/__init__.py @@ -1,6 +1,6 @@ -from retry import * +from retry import retryable_request, post_request __all__ = { "retryable_request", "post_request" -} \ No newline at end of file +} From eddaece5ffc741eb6626d26d12d9fbd429e9ad06 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:23:34 +0300 Subject: [PATCH 11/12] Update retry.py --- mpesakit/retry/retry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpesakit/retry/retry.py b/mpesakit/retry/retry.py index 5f9d279..6f1a30d 100644 --- a/mpesakit/retry/retry.py +++ b/mpesakit/retry/retry.py @@ -15,7 +15,7 @@ import logging logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) +logger.addHandler(logging.NullHandler()) def retryable_request( From 03bbd32dcbb0d6bbcba25cf957a728b477ac7349 Mon Sep 17 00:00:00 2001 From: Kennedy <145441761+Dravis-Xam@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:25:05 +0300 Subject: [PATCH 12/12] Update retry.py --- tests/integration/retry/retry.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/integration/retry/retry.py b/tests/integration/retry/retry.py index 69e5c30..50a2c95 100644 --- a/tests/integration/retry/retry.py +++ b/tests/integration/retry/retry.py @@ -1,16 +1,13 @@ # tests/test_retry.py import pytest from mpesakit.retry.retry import retryable_request -import random +attempt_counter = {"count": 0} + -# A dummy function to simulate failure @retryable_request(stop_attempts=5, wait_min=0, wait_max=0, exception_type=ValueError) def sometimes_fails(): - """Fails randomly to test retry logic.""" - if random.random() < 0.7: - raise ValueError("Random failure") +"""Fails deterministically to test retry logic.""" + attempt_counter["count"] += 1 + if attempt_counter["count"] < 5: + raise ValueError("Planned failure") return "Success" - -def test_retryable_request(): - result = sometimes_fails() - assert result == "Success"