Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@ Several HTTP errors are often transient, and might succeed if retried:

This project aims to simplify retrying these, by extending [`tenacity`](https://tenacity.readthedocs.io/) with custom retry and wait strategies, as well as a custom decorator. Defaults are sensible for most use cases, but are fully customizable.

Supports both [`requests`](https://docs.python-requests.org/en/latest/index.html) and [`httpx`](https://python-httpx.org/) natively, but could be customized to use with any library that raises exceptions for the conditions listed above.
Supports both [`requests`](https://docs.python-requests.org/en/latest/index.html), [`httpx`](https://python-httpx.org/) and [`aiohttp`](https://docs.aiohttp.org/) natively, but could be customized to use with any library that raises exceptions for the conditions listed above.

## Install

Install from PyPI:

```sh
pip install retryhttp[all] # Supports both HTTPX and requests
pip install retryhttp # Supports HTTPX, requests and aiohttp
```

You can also install support for only HTTPX or requests, if you would rather not install unnecessary dependencies:

```sh
pip install retryhttp[httpx] # Supports only HTTPX
pip install retryhttp[requests] # Supports only requests
pip install retryhttp[aiohttp] # Supports only aiohttp
```

Or, install the latest development snapshot from git:
Expand Down
4 changes: 2 additions & 2 deletions docs/guide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Overview

`retryhttp` makes it easy to retry potentially transient HTTP errors when using `httpx` or `requests`.
`retryhttp` makes it easy to retry potentially transient HTTP errors when using `httpx`, `requests` or `aiohttp`.

!!! note
Errors that you can safely retry vary from service to service.
Expand Down Expand Up @@ -83,4 +83,4 @@ Under the hood, RetryHTTP is a convenience layer on top of the excellent retry l

`tenacity` works by adding a decorator to functions that might fail. This decorator is configured with retry, wait, and stop strategies that configure what conditions to retry, how long to wait between retries, and when to stop retrying, respectively. Failures could be a raised exception, or a configurable return value. See [`tenacity` documentation](https://tenacity.readthedocs.io/en/latest/index.html) for details.

`retryhttp` provides new retry and stop strategies for potentially transient error conditions raised by `httpx` and `requests`. To make things as convenient as possible, `retryhttp` also provides a [new decorator][retryhttp.retry] that wraps [`tenacity.retry`](https://tenacity.readthedocs.io/en/latest/api.html#tenacity.retry) with sensible defaults, which are all customizable.
`retryhttp` provides new retry and stop strategies for potentially transient error conditions raised by `httpx`, `requests` and `aiohttp`. To make things as convenient as possible, `retryhttp` also provides a [new decorator][retryhttp.retry] that wraps [`tenacity.retry`](https://tenacity.readthedocs.io/en/latest/api.html#tenacity.retry) with sensible defaults, which are all customizable.
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Several HTTP errors are often transient, and might succeed if retried:

This project aims to simplify retrying these, by extending [`tenacity`](https://tenacity.readthedocs.io/) with custom retry and wait strategies, as well as a custom decorator. Defaults are sensible for most use cases, but are fully customizable.

Supports exceptions raised by both [`requests`](https://docs.python-requests.org/en/latest/index.html) and [`httpx`](https://python-httpx.org/).
Supports exceptions raised by both [`requests`](https://docs.python-requests.org/en/latest/index.html), [`httpx`](https://python-httpx.org/) and [`aiohttp`](https://docs.aiohttp.org/).

## Install

Install from PyPI:

```sh
# Supports both HTTPX and requests
# Supports HTTPX, requests and aiohttp
pip install retryhttp
```

Expand All @@ -33,6 +33,7 @@ You can also install support for only HTTPX or requests:
```sh
pip install retryhttp[httpx] # Supports only HTTPX
pip install retryhttp[requests] # Supports only requests
pip install retryhttp[aiohttp] # Supports only aiohttp
```

Or, install the latest development snapshot from git:
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ all = [
"requests",
"types-requests",
]
aiohttp = [
"aiohttp",
"tenacity",
]
dev = [
"pytest",
"pytest-xdist",
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
httpx
tenacity
pydantic
aiohttp
requests
types-requests
7 changes: 7 additions & 0 deletions retryhttp/_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ def retry(
- `httpx.WriteError`
- `requests.ConnectionError`
- `requests.exceptions.ChunkedEncodingError`
- `aiohttp.ClientConnectionError`
- Timeouts:
- `httpx.TimeoutException`
- `requests.Timeout`
- `aiohttp.ServerTimeoutError`

Args:
max_attempt_number: Total times to attempt a request. Includes the first attempt
Expand All @@ -112,11 +114,13 @@ def retry(
- `httpx.WriteError`
- `requests.ConnectError`
- `requests.exceptions.ChunkedEncodingError`
- `aiohttp.ClientConnectionError`
timeouts: One or more exceptions that will trigger `wait_timeouts` if
`retry_timeouts` is `True`. Defaults to:

- `httpx.TimeoutException`
- `requests.Timeout`
- `aiohttp.ServerTimeoutError`

Returns:
Decorated function.
Expand Down Expand Up @@ -181,6 +185,7 @@ class retry_if_network_error(retry_if_exception_type):
- `httpx.WriteError`
- `requests.ConnectionError`
- `requests.exceptions.ChunkedEncodingError`
- `aiohttp.ClientConnectionError`

"""

Expand Down Expand Up @@ -237,6 +242,8 @@ class retry_if_timeout(retry_if_exception_type):
- `httpx.ReadTimeout`
- `httpx.WriteTimeout`
- `requests.Timeout`
- `aiohttp.ClientConnectionError`

"""

def __init__(
Expand Down
18 changes: 18 additions & 0 deletions retryhttp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

_HTTPX_INSTALLED = False
_REQUESTS_INSTALLED = False
_AIOHTTP_INSTALLED = False

try:
import httpx
Expand All @@ -21,6 +22,13 @@
except ImportError:
pass

try:
import aiohttp

_AIOHTTP_INSTALLED = True
except ImportError:
pass


def get_default_network_errors() -> Tuple[Type[BaseException], ...]:
"""Get all network errors to use by default.
Expand Down Expand Up @@ -51,6 +59,8 @@ def get_default_network_errors() -> Tuple[Type[BaseException], ...]:
requests.exceptions.ChunkedEncodingError,
]
)
if _AIOHTTP_INSTALLED:
exceptions.append(aiohttp.ClientConnectionError)
return tuple(exceptions)


Expand All @@ -66,6 +76,8 @@ def get_default_timeouts() -> Tuple[Type[BaseException], ...]:
exceptions.append(httpx.TimeoutException)
if _REQUESTS_INSTALLED:
exceptions.append(requests.Timeout)
if _AIOHTTP_INSTALLED:
exceptions.append(aiohttp.ServerTimeoutError)
return tuple(exceptions)


Expand All @@ -81,6 +93,8 @@ def get_default_http_status_exceptions() -> Tuple[Type[BaseException], ...]:
exceptions.append(httpx.HTTPStatusError)
if _REQUESTS_INSTALLED:
exceptions.append(requests.HTTPError)
if _AIOHTTP_INSTALLED:
exceptions.append(aiohttp.ClientResponseError)
return tuple(exceptions)


Expand All @@ -99,6 +113,8 @@ def is_rate_limited(exc: Optional[BaseException]) -> bool:
exceptions = get_default_http_status_exceptions()
if isinstance(exc, exceptions) and hasattr(exc, "response"):
return exc.response.status_code == 429
if isinstance(exc, exceptions) and hasattr(exc, "status"): # for aiohttp.ClientResponseError
return exc.status == 429
return False


Expand All @@ -124,6 +140,8 @@ def is_server_error(
exceptions = get_default_http_status_exceptions()
if isinstance(exc, exceptions) and hasattr(exc, "response"):
return exc.response.status_code in status_codes
if isinstance(exc, exceptions) and hasattr(exc, "status"): # for aiohttp.ClientResponseError
return exc.status in status_codes
return False


Expand Down
40 changes: 22 additions & 18 deletions retryhttp/_wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,26 @@ def _get_wait_value(self, retry_state: RetryCallState) -> float:
exc = retry_state.outcome.exception()
if exc is None:
return 0
if isinstance(exc, get_default_http_status_exceptions()) and hasattr(
exc, "response"
):
value = exc.response.headers.get(self.header)
if value is None:
raise ValueError(f"Header not present: {self.header}")
if re.match(r"^\d+$", value):
return float(value)
else:
retry_after = datetime.strptime(value, HTTP_DATE_FORMAT)
retry_after = retry_after.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
if retry_after < now:
raise ValueError(
f'Date provided in header "{self.header}" '
f"is in the past: {value}"
)
return float((retry_after - now).seconds)
value = None
if isinstance(exc, get_default_http_status_exceptions()):
if hasattr(exc, "response"):
value = exc.response.headers.get(self.header)
elif hasattr(exc, "status"): # for aiohttp.ClientResponseError
value = exc.headers.get(self.header)
if value is None:
raise ValueError(f"Header not present: {self.header}")
if re.match(r"^\d+$", value):
return float(value)
else:
retry_after = datetime.strptime(value, HTTP_DATE_FORMAT)
retry_after = retry_after.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
if retry_after < now:
raise ValueError(
f'Date provided in header "{self.header}" '
f"is in the past: {value}"
)
return float((retry_after - now).seconds)
raise ValueError(f'Unable to parse wait time from header: "{self.header}"')

def __call__(self, retry_state: RetryCallState) -> float:
Expand Down Expand Up @@ -162,13 +164,15 @@ class wait_context_aware(wait_base):
- `httpx.WriteError`
- `requests.ConnectionError`
- `requests.exceptions.ChunkedEncodingError`
- `aiohttp.ClientConnectionError`
timeouts: One or more exceptions that will trigger `wait_timeouts`. If omitted,
defaults to:

- `httpx.ConnectTimeout`
- `httpx.ReadTimeout`
- `httpx.WriteTimeout`
- `requests.Timeout`
- `aiohttp.ServerTimeoutError`

"""

Expand Down