swarmauri_transport_retry 0.1.0.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swarmauri_transport_retry-0.1.0.dev1/LICENSE +13 -0
- swarmauri_transport_retry-0.1.0.dev1/PKG-INFO +75 -0
- swarmauri_transport_retry-0.1.0.dev1/README.md +44 -0
- swarmauri_transport_retry-0.1.0.dev1/pyproject.toml +36 -0
- swarmauri_transport_retry-0.1.0.dev1/swarmauri_transport_retry/ExponentialBackoffRetryPolicy.py +113 -0
- swarmauri_transport_retry-0.1.0.dev1/swarmauri_transport_retry/RetryingHttpClient.py +208 -0
- swarmauri_transport_retry-0.1.0.dev1/swarmauri_transport_retry/__init__.py +4 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2026 Jacob Stewart
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swarmauri_transport_retry
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
|
+
Summary: Policy-driven synchronous, asynchronous, and streaming HTTP retries for Swarmauri clients.
|
|
5
|
+
License: Copyright 2026 Jacob Stewart
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Keywords: swarmauri,http,retry,backoff,transport,async
|
|
20
|
+
Author: Jacob Stewart
|
|
21
|
+
Author-email: jacob@swarmauri.com
|
|
22
|
+
Requires-Python: >=3.10,<3.13
|
|
23
|
+
Classifier: Development Status :: 1 - Planning
|
|
24
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
28
|
+
Requires-Dist: swarmauri_core (>=0.11.0.dev5)
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/swarmauri_transport_retry/) [](https://pypi.org/project/swarmauri_transport_retry/) [](LICENSE) [](https://pepy.tech/project/swarmauri_transport_retry) [](https://hits.sh/github.com/swarmauri/swarmauri-sdk/)
|
|
34
|
+
|
|
35
|
+
# swarmauri_transport_retry
|
|
36
|
+
|
|
37
|
+
Policy-driven retries for any client implementing Swarmauri's `IHttpClient` contract.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- Bounded exponential backoff with optional jitter.
|
|
42
|
+
- `Retry-After` support for seconds and HTTP dates.
|
|
43
|
+
- Separate policy decisions from sync, async, and streaming execution.
|
|
44
|
+
- Protect non-idempotent methods from replay by default.
|
|
45
|
+
- Wrap any Swarmauri HTTP client without depending on its HTTP library.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv add swarmauri_transport_retry
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install swarmauri_transport_retry
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from swarmauri_transport_httpx import HttpxClient
|
|
61
|
+
from swarmauri_transport_retry import (
|
|
62
|
+
ExponentialBackoffRetryPolicy,
|
|
63
|
+
RetryingHttpClient,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
client = RetryingHttpClient(
|
|
67
|
+
client=HttpxClient(base_url="https://api.example.com"),
|
|
68
|
+
policy=ExponentialBackoffRetryPolicy(max_attempts=3),
|
|
69
|
+
)
|
|
70
|
+
response = client.request("GET", "/models")
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The retry policy controls replay safety and delay. The wrapped transport remains responsible for moving HTTP requests and responses.
|
|
75
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/swarmauri_transport_retry/) [](https://pypi.org/project/swarmauri_transport_retry/) [](LICENSE) [](https://pepy.tech/project/swarmauri_transport_retry) [](https://hits.sh/github.com/swarmauri/swarmauri-sdk/)
|
|
4
|
+
|
|
5
|
+
# swarmauri_transport_retry
|
|
6
|
+
|
|
7
|
+
Policy-driven retries for any client implementing Swarmauri's `IHttpClient` contract.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Bounded exponential backoff with optional jitter.
|
|
12
|
+
- `Retry-After` support for seconds and HTTP dates.
|
|
13
|
+
- Separate policy decisions from sync, async, and streaming execution.
|
|
14
|
+
- Protect non-idempotent methods from replay by default.
|
|
15
|
+
- Wrap any Swarmauri HTTP client without depending on its HTTP library.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv add swarmauri_transport_retry
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install swarmauri_transport_retry
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from swarmauri_transport_httpx import HttpxClient
|
|
31
|
+
from swarmauri_transport_retry import (
|
|
32
|
+
ExponentialBackoffRetryPolicy,
|
|
33
|
+
RetryingHttpClient,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
client = RetryingHttpClient(
|
|
37
|
+
client=HttpxClient(base_url="https://api.example.com"),
|
|
38
|
+
policy=ExponentialBackoffRetryPolicy(max_attempts=3),
|
|
39
|
+
)
|
|
40
|
+
response = client.request("GET", "/models")
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The retry policy controls replay safety and delay. The wrapped transport remains responsible for moving HTTP requests and responses.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "swarmauri_transport_retry"
|
|
3
|
+
version = "0.1.0.dev1"
|
|
4
|
+
description = "Policy-driven synchronous, asynchronous, and streaming HTTP retries for Swarmauri clients."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10,<3.13"
|
|
7
|
+
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
8
|
+
license = { file = "LICENSE" }
|
|
9
|
+
dependencies = ["swarmauri_core>=0.11.0.dev5"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 1 - Planning",
|
|
12
|
+
"License :: OSI Approved :: Apache Software License",
|
|
13
|
+
"Programming Language :: Python :: 3.10",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["swarmauri", "http", "retry", "backoff", "transport", "async"]
|
|
18
|
+
|
|
19
|
+
[tool.uv.sources]
|
|
20
|
+
swarmauri_core = { workspace = true }
|
|
21
|
+
|
|
22
|
+
[project.entry-points."swarmauri.transports"]
|
|
23
|
+
RetryingHttpClient = "swarmauri_transport_retry:RetryingHttpClient"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["poetry-core>=1.8.0"]
|
|
27
|
+
build-backend = "poetry.core.masonry.api"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.24", "ruff>=0.9.9"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
extend = "../../pyproject.toml"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
ignore = ["N999"]
|
swarmauri_transport_retry-0.1.0.dev1/swarmauri_transport_retry/ExponentialBackoffRetryPolicy.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Concrete exponential-backoff policy for HTTP operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import Collection
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from email.utils import parsedate_to_datetime
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from swarmauri_core.transports.i_http_response import IHttpResponse
|
|
12
|
+
from swarmauri_core.transports.i_http_retry_policy import IHttpRetryPolicy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExponentialBackoffRetryPolicy(IHttpRetryPolicy):
|
|
16
|
+
"""Retry safe HTTP operations with bounded exponential backoff."""
|
|
17
|
+
|
|
18
|
+
type: Literal["ExponentialBackoffRetryPolicy"] = (
|
|
19
|
+
"ExponentialBackoffRetryPolicy"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
max_attempts: int = 3,
|
|
26
|
+
retryable_status_codes: Collection[int] = (429, 500, 502, 503, 504),
|
|
27
|
+
retryable_methods: Collection[str] = (
|
|
28
|
+
"GET",
|
|
29
|
+
"HEAD",
|
|
30
|
+
"OPTIONS",
|
|
31
|
+
"PUT",
|
|
32
|
+
"DELETE",
|
|
33
|
+
),
|
|
34
|
+
base_delay: float = 1.0,
|
|
35
|
+
maximum_delay: float = 30.0,
|
|
36
|
+
respect_retry_after: bool = True,
|
|
37
|
+
retry_request_errors: bool = True,
|
|
38
|
+
jitter_ratio: float = 0.0,
|
|
39
|
+
) -> None:
|
|
40
|
+
if max_attempts < 1:
|
|
41
|
+
raise ValueError("max_attempts must be at least 1")
|
|
42
|
+
if base_delay < 0 or maximum_delay < 0:
|
|
43
|
+
raise ValueError("retry delays cannot be negative")
|
|
44
|
+
if not 0 <= jitter_ratio <= 1:
|
|
45
|
+
raise ValueError("jitter_ratio must be between 0 and 1")
|
|
46
|
+
self.max_attempts = max_attempts
|
|
47
|
+
self.retryable_status_codes = frozenset(retryable_status_codes)
|
|
48
|
+
self.retryable_methods = frozenset(
|
|
49
|
+
method.upper() for method in retryable_methods
|
|
50
|
+
)
|
|
51
|
+
self.base_delay = base_delay
|
|
52
|
+
self.maximum_delay = maximum_delay
|
|
53
|
+
self.respect_retry_after = respect_retry_after
|
|
54
|
+
self.retry_request_errors = retry_request_errors
|
|
55
|
+
self.jitter_ratio = jitter_ratio
|
|
56
|
+
|
|
57
|
+
def should_retry(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
method: str,
|
|
61
|
+
attempt: int,
|
|
62
|
+
response: IHttpResponse | None = None,
|
|
63
|
+
error: Exception | None = None,
|
|
64
|
+
) -> bool:
|
|
65
|
+
if attempt >= self.max_attempts:
|
|
66
|
+
return False
|
|
67
|
+
if method.upper() not in self.retryable_methods:
|
|
68
|
+
return False
|
|
69
|
+
if error is not None:
|
|
70
|
+
return self.retry_request_errors
|
|
71
|
+
return (
|
|
72
|
+
response is not None
|
|
73
|
+
and response.status_code in self.retryable_status_codes
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def delay_seconds(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
attempt: int,
|
|
80
|
+
response: IHttpResponse | None = None,
|
|
81
|
+
) -> float:
|
|
82
|
+
retry_after = self._retry_after_seconds(response)
|
|
83
|
+
if retry_after is not None:
|
|
84
|
+
delay = retry_after
|
|
85
|
+
else:
|
|
86
|
+
delay = self.base_delay * (2 ** max(attempt - 1, 0))
|
|
87
|
+
delay = min(delay, self.maximum_delay)
|
|
88
|
+
if self.jitter_ratio:
|
|
89
|
+
spread = delay * self.jitter_ratio
|
|
90
|
+
delay = random.uniform(max(0.0, delay - spread), delay + spread)
|
|
91
|
+
return max(0.0, min(delay, self.maximum_delay))
|
|
92
|
+
|
|
93
|
+
def _retry_after_seconds(
|
|
94
|
+
self, response: IHttpResponse | None
|
|
95
|
+
) -> float | None:
|
|
96
|
+
if not self.respect_retry_after or response is None:
|
|
97
|
+
return None
|
|
98
|
+
value = response.headers.get("Retry-After")
|
|
99
|
+
if not value:
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
return max(float(value), 0.0)
|
|
103
|
+
except ValueError:
|
|
104
|
+
try:
|
|
105
|
+
retry_at = parsedate_to_datetime(value)
|
|
106
|
+
except (TypeError, ValueError):
|
|
107
|
+
return None
|
|
108
|
+
if retry_at.tzinfo is None:
|
|
109
|
+
retry_at = retry_at.replace(tzinfo=timezone.utc)
|
|
110
|
+
return max(
|
|
111
|
+
(retry_at - datetime.now(timezone.utc)).total_seconds(),
|
|
112
|
+
0.0,
|
|
113
|
+
)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""HTTP client decorator that executes concrete retry policies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncIterator, Iterator, Mapping
|
|
8
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from swarmauri_core.transports.http_errors import HttpRequestError
|
|
12
|
+
from swarmauri_core.transports.i_http_client import IHttpClient
|
|
13
|
+
from swarmauri_core.transports.i_http_response import IHttpResponse
|
|
14
|
+
from swarmauri_core.transports.i_http_retry_policy import IHttpRetryPolicy
|
|
15
|
+
from swarmauri_core.transports.i_http_stream_response import (
|
|
16
|
+
IAsyncHttpStreamResponse,
|
|
17
|
+
IHttpStreamResponse,
|
|
18
|
+
)
|
|
19
|
+
from swarmauri_core.transports.i_managed_http_client import IManagedHttpClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RetryingHttpClient(IManagedHttpClient):
|
|
23
|
+
"""Decorate any Swarmauri HTTP client with an explicit retry policy."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
client: IHttpClient,
|
|
29
|
+
policy: IHttpRetryPolicy,
|
|
30
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
31
|
+
async_sleep: Callable[[float], Any] = asyncio.sleep,
|
|
32
|
+
own_client: bool = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.client = client
|
|
35
|
+
self.policy = policy
|
|
36
|
+
self._sleep = sleep
|
|
37
|
+
self._async_sleep = async_sleep
|
|
38
|
+
self.own_client = own_client
|
|
39
|
+
|
|
40
|
+
def request(
|
|
41
|
+
self,
|
|
42
|
+
method: str,
|
|
43
|
+
path: str,
|
|
44
|
+
*,
|
|
45
|
+
headers: Mapping[str, str] | None = None,
|
|
46
|
+
body: bytes = b"",
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> IHttpResponse:
|
|
49
|
+
attempt = 1
|
|
50
|
+
while True:
|
|
51
|
+
try:
|
|
52
|
+
response = self.client.request(
|
|
53
|
+
method,
|
|
54
|
+
path,
|
|
55
|
+
headers=headers,
|
|
56
|
+
body=body,
|
|
57
|
+
**kwargs,
|
|
58
|
+
)
|
|
59
|
+
except HttpRequestError as error:
|
|
60
|
+
if not self.policy.should_retry(
|
|
61
|
+
method=method, attempt=attempt, error=error
|
|
62
|
+
):
|
|
63
|
+
raise
|
|
64
|
+
self._sleep(self.policy.delay_seconds(attempt=attempt))
|
|
65
|
+
else:
|
|
66
|
+
if not self.policy.should_retry(
|
|
67
|
+
method=method, attempt=attempt, response=response
|
|
68
|
+
):
|
|
69
|
+
return response
|
|
70
|
+
self._sleep(
|
|
71
|
+
self.policy.delay_seconds(
|
|
72
|
+
attempt=attempt, response=response
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
attempt += 1
|
|
76
|
+
|
|
77
|
+
async def arequest(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
*,
|
|
82
|
+
headers: Mapping[str, str] | None = None,
|
|
83
|
+
body: bytes = b"",
|
|
84
|
+
**kwargs: Any,
|
|
85
|
+
) -> IHttpResponse:
|
|
86
|
+
attempt = 1
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
response = await self.client.arequest(
|
|
90
|
+
method,
|
|
91
|
+
path,
|
|
92
|
+
headers=headers,
|
|
93
|
+
body=body,
|
|
94
|
+
**kwargs,
|
|
95
|
+
)
|
|
96
|
+
except HttpRequestError as error:
|
|
97
|
+
if not self.policy.should_retry(
|
|
98
|
+
method=method, attempt=attempt, error=error
|
|
99
|
+
):
|
|
100
|
+
raise
|
|
101
|
+
await self._async_sleep(
|
|
102
|
+
self.policy.delay_seconds(attempt=attempt)
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
if not self.policy.should_retry(
|
|
106
|
+
method=method, attempt=attempt, response=response
|
|
107
|
+
):
|
|
108
|
+
return response
|
|
109
|
+
await self._async_sleep(
|
|
110
|
+
self.policy.delay_seconds(
|
|
111
|
+
attempt=attempt, response=response
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
attempt += 1
|
|
115
|
+
|
|
116
|
+
@contextmanager
|
|
117
|
+
def stream(
|
|
118
|
+
self,
|
|
119
|
+
method: str,
|
|
120
|
+
path: str,
|
|
121
|
+
*,
|
|
122
|
+
headers: Mapping[str, str] | None = None,
|
|
123
|
+
body: bytes = b"",
|
|
124
|
+
**kwargs: Any,
|
|
125
|
+
) -> Iterator[IHttpStreamResponse]:
|
|
126
|
+
attempt = 1
|
|
127
|
+
while True:
|
|
128
|
+
try:
|
|
129
|
+
with self.client.stream(
|
|
130
|
+
method,
|
|
131
|
+
path,
|
|
132
|
+
headers=headers,
|
|
133
|
+
body=body,
|
|
134
|
+
**kwargs,
|
|
135
|
+
) as response:
|
|
136
|
+
if not self.policy.should_retry(
|
|
137
|
+
method=method, attempt=attempt, response=response
|
|
138
|
+
):
|
|
139
|
+
yield response
|
|
140
|
+
return
|
|
141
|
+
delay = self.policy.delay_seconds(
|
|
142
|
+
attempt=attempt, response=response
|
|
143
|
+
)
|
|
144
|
+
except HttpRequestError as error:
|
|
145
|
+
if not self.policy.should_retry(
|
|
146
|
+
method=method, attempt=attempt, error=error
|
|
147
|
+
):
|
|
148
|
+
raise
|
|
149
|
+
delay = self.policy.delay_seconds(attempt=attempt)
|
|
150
|
+
self._sleep(delay)
|
|
151
|
+
attempt += 1
|
|
152
|
+
|
|
153
|
+
@asynccontextmanager
|
|
154
|
+
async def astream(
|
|
155
|
+
self,
|
|
156
|
+
method: str,
|
|
157
|
+
path: str,
|
|
158
|
+
*,
|
|
159
|
+
headers: Mapping[str, str] | None = None,
|
|
160
|
+
body: bytes = b"",
|
|
161
|
+
**kwargs: Any,
|
|
162
|
+
) -> AsyncIterator[IAsyncHttpStreamResponse]:
|
|
163
|
+
attempt = 1
|
|
164
|
+
while True:
|
|
165
|
+
try:
|
|
166
|
+
async with self.client.astream(
|
|
167
|
+
method,
|
|
168
|
+
path,
|
|
169
|
+
headers=headers,
|
|
170
|
+
body=body,
|
|
171
|
+
**kwargs,
|
|
172
|
+
) as response:
|
|
173
|
+
if not self.policy.should_retry(
|
|
174
|
+
method=method, attempt=attempt, response=response
|
|
175
|
+
):
|
|
176
|
+
yield response
|
|
177
|
+
return
|
|
178
|
+
delay = self.policy.delay_seconds(
|
|
179
|
+
attempt=attempt, response=response
|
|
180
|
+
)
|
|
181
|
+
except HttpRequestError as error:
|
|
182
|
+
if not self.policy.should_retry(
|
|
183
|
+
method=method, attempt=attempt, error=error
|
|
184
|
+
):
|
|
185
|
+
raise
|
|
186
|
+
delay = self.policy.delay_seconds(attempt=attempt)
|
|
187
|
+
await self._async_sleep(delay)
|
|
188
|
+
attempt += 1
|
|
189
|
+
|
|
190
|
+
def close(self) -> None:
|
|
191
|
+
if self.own_client and isinstance(self.client, IManagedHttpClient):
|
|
192
|
+
self.client.close()
|
|
193
|
+
|
|
194
|
+
async def aclose(self) -> None:
|
|
195
|
+
if self.own_client and isinstance(self.client, IManagedHttpClient):
|
|
196
|
+
await self.client.aclose()
|
|
197
|
+
|
|
198
|
+
def __enter__(self) -> "RetryingHttpClient":
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
def __exit__(self, *args: Any) -> None:
|
|
202
|
+
self.close()
|
|
203
|
+
|
|
204
|
+
async def __aenter__(self) -> "RetryingHttpClient":
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
208
|
+
await self.aclose()
|