Skip to content
6 changes: 6 additions & 0 deletions mpesakit/retry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from retry import retryable_request, post_request

__all__ = {
"retryable_request",
"post_request"
}
55 changes: 55 additions & 0 deletions mpesakit/retry/retry.py
Original file line number Diff line number Diff line change
@@ -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__)
logger.addHandler(logging.NullHandler())


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()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Empty file.
13 changes: 13 additions & 0 deletions tests/integration/retry/retry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# tests/test_retry.py
import pytest
from mpesakit.retry.retry import retryable_request
attempt_counter = {"count": 0}


@retryable_request(stop_attempts=5, wait_min=0, wait_max=0, exception_type=ValueError)
def sometimes_fails():
"""Fails deterministically to test retry logic."""
attempt_counter["count"] += 1
if attempt_counter["count"] < 5:
raise ValueError("Planned failure")
return "Success"