port-ocean 0.12.2.dev14__py3-none-any.whl → 0.12.2.dev16__py3-none-any.whl
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.
Potentially problematic release.
This version of port-ocean might be problematic. Click here for more details.
- port_ocean/cli/commands/list_integrations.py +5 -8
- port_ocean/cli/commands/pull.py +16 -20
- port_ocean/clients/port/authentication.py +13 -12
- port_ocean/clients/port/client.py +8 -9
- port_ocean/clients/port/mixins/blueprints.py +14 -14
- port_ocean/clients/port/mixins/entities.py +8 -7
- port_ocean/clients/port/mixins/integrations.py +11 -11
- port_ocean/clients/port/mixins/migrations.py +5 -5
- port_ocean/clients/port/retry_transport.py +35 -13
- port_ocean/clients/port/utils.py +23 -32
- port_ocean/core/defaults/clean.py +3 -3
- port_ocean/core/defaults/common.py +3 -3
- port_ocean/core/defaults/initialize.py +47 -48
- port_ocean/core/integrations/mixins/sync_raw.py +3 -3
- port_ocean/core/utils.py +3 -4
- port_ocean/helpers/async_client.py +53 -0
- port_ocean/helpers/retry.py +221 -71
- port_ocean/ocean.py +22 -20
- port_ocean/run.py +15 -20
- port_ocean/tests/clients/port/mixins/test_entities.py +4 -3
- port_ocean/tests/test_smoke.py +3 -3
- port_ocean/utils/async_http.py +8 -13
- port_ocean/utils/repeat.py +2 -6
- port_ocean/utils/signal.py +2 -1
- {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev16.dist-info}/METADATA +1 -2
- {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev16.dist-info}/RECORD +29 -28
- {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev16.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev16.dist-info}/WHEEL +0 -0
- {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev16.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from typing import Type, Any
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import httpx
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
7
|
from port_ocean.clients.port.client import PortClient
|
|
@@ -21,7 +21,7 @@ from port_ocean.exceptions.port_defaults import (
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def deconstruct_blueprints_to_creation_steps(
|
|
24
|
-
|
|
24
|
+
raw_blueprints: list[dict[str, Any]],
|
|
25
25
|
) -> tuple[list[dict[str, Any]], ...]:
|
|
26
26
|
"""
|
|
27
27
|
Deconstructing the blueprint into stages so the api wont fail to create a blueprint if there is a conflict
|
|
@@ -53,9 +53,9 @@ def deconstruct_blueprints_to_creation_steps(
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
async def _initialize_required_integration_settings(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
port_client: PortClient,
|
|
57
|
+
default_mapping: PortAppConfig,
|
|
58
|
+
integration_config: IntegrationConfiguration,
|
|
59
59
|
) -> None:
|
|
60
60
|
try:
|
|
61
61
|
logger.info("Initializing integration at port")
|
|
@@ -80,8 +80,8 @@ async def _initialize_required_integration_settings(
|
|
|
80
80
|
integration_config.event_listener.to_request(),
|
|
81
81
|
port_app_config=default_mapping,
|
|
82
82
|
)
|
|
83
|
-
except
|
|
84
|
-
logger.error(f"Failed to apply default mapping: {err.
|
|
83
|
+
except httpx.HTTPStatusError as err:
|
|
84
|
+
logger.error(f"Failed to apply default mapping: {err.response.text}.")
|
|
85
85
|
raise err
|
|
86
86
|
|
|
87
87
|
logger.info("Checking for diff in integration configuration")
|
|
@@ -89,9 +89,9 @@ async def _initialize_required_integration_settings(
|
|
|
89
89
|
"changelog_destination"
|
|
90
90
|
)
|
|
91
91
|
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
integration.get("changelogDestination") != changelog_destination
|
|
93
|
+
or integration.get("installationAppType") != integration_config.integration.type
|
|
94
|
+
or integration.get("version") != port_client.integration_version
|
|
95
95
|
):
|
|
96
96
|
await port_client.patch_integration(
|
|
97
97
|
integration_config.integration.type, changelog_destination
|
|
@@ -99,8 +99,8 @@ async def _initialize_required_integration_settings(
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
async def _create_resources(
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
port_client: PortClient,
|
|
103
|
+
defaults: Defaults,
|
|
104
104
|
) -> None:
|
|
105
105
|
creation_stage, *blueprint_patches = deconstruct_blueprints_to_creation_steps(
|
|
106
106
|
defaults.blueprints
|
|
@@ -133,9 +133,9 @@ async def _create_resources(
|
|
|
133
133
|
|
|
134
134
|
if blueprint_errors:
|
|
135
135
|
for error in blueprint_errors:
|
|
136
|
-
if isinstance(error,
|
|
136
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
137
137
|
logger.warning(
|
|
138
|
-
f"Failed to create resources: {error.
|
|
138
|
+
f"Failed to create resources: {error.response.text}. Rolling back changes..."
|
|
139
139
|
)
|
|
140
140
|
|
|
141
141
|
raise AbortDefaultCreationError(
|
|
@@ -155,8 +155,8 @@ async def _create_resources(
|
|
|
155
155
|
)
|
|
156
156
|
)
|
|
157
157
|
|
|
158
|
-
except
|
|
159
|
-
logger.error(f"Failed to create resources: {err.
|
|
158
|
+
except httpx.HTTPStatusError as err:
|
|
159
|
+
logger.error(f"Failed to create resources: {err.response.text}. continuing...")
|
|
160
160
|
raise AbortDefaultCreationError(created_blueprints_identifiers, [err])
|
|
161
161
|
try:
|
|
162
162
|
created_actions, actions_errors = await gather_and_split_errors_from_results(
|
|
@@ -185,9 +185,9 @@ async def _create_resources(
|
|
|
185
185
|
errors = actions_errors + scorecards_errors + pages_errors
|
|
186
186
|
if errors:
|
|
187
187
|
for error in errors:
|
|
188
|
-
if isinstance(error,
|
|
188
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
189
189
|
logger.warning(
|
|
190
|
-
f"Failed to create resource: {error.
|
|
190
|
+
f"Failed to create resource: {error.response.text}. continuing..."
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
except Exception as err:
|
|
@@ -195,45 +195,44 @@ async def _create_resources(
|
|
|
195
195
|
|
|
196
196
|
|
|
197
197
|
async def _initialize_defaults(
|
|
198
|
-
|
|
198
|
+
config_class: Type[PortAppConfig], integration_config: IntegrationConfiguration
|
|
199
199
|
) -> None:
|
|
200
200
|
port_client = ocean.port_client
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
201
|
+
defaults = get_port_integration_defaults(config_class)
|
|
202
|
+
if not defaults:
|
|
203
|
+
logger.warning("No defaults found. Skipping initialization...")
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
if defaults.port_app_config:
|
|
207
|
+
await _initialize_required_integration_settings(
|
|
208
|
+
port_client, defaults.port_app_config, integration_config
|
|
209
|
+
)
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
if not integration_config.initialize_port_resources:
|
|
212
|
+
return
|
|
214
213
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
for identifier in e.blueprints_to_rollback
|
|
214
|
+
try:
|
|
215
|
+
logger.info("Found default resources, starting creation process")
|
|
216
|
+
await _create_resources(port_client, defaults)
|
|
217
|
+
except AbortDefaultCreationError as e:
|
|
218
|
+
logger.warning(
|
|
219
|
+
f"Failed to create resources. Rolling back blueprints : {e.blueprints_to_rollback}"
|
|
220
|
+
)
|
|
221
|
+
await asyncio.gather(
|
|
222
|
+
*(
|
|
223
|
+
port_client.delete_blueprint(
|
|
224
|
+
identifier,
|
|
225
|
+
should_raise=False,
|
|
226
|
+
user_agent_type=UserAgentType.exporter,
|
|
230
227
|
)
|
|
228
|
+
for identifier in e.blueprints_to_rollback
|
|
231
229
|
)
|
|
232
|
-
|
|
230
|
+
)
|
|
231
|
+
raise ExceptionGroup(str(e), e.errors)
|
|
233
232
|
|
|
234
233
|
|
|
235
234
|
def initialize_defaults(
|
|
236
|
-
|
|
235
|
+
config_class: Type[PortAppConfig], integration_config: IntegrationConfiguration
|
|
237
236
|
) -> None:
|
|
238
237
|
asyncio.new_event_loop().run_until_complete(
|
|
239
238
|
_initialize_defaults(config_class, integration_config)
|
|
@@ -431,13 +431,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
431
431
|
# entities_at_port = await ocean.port_client.search_entities(
|
|
432
432
|
# user_agent_type
|
|
433
433
|
# )
|
|
434
|
-
# except
|
|
434
|
+
# except httpx.HTTPError as e:
|
|
435
435
|
# logger.warning(
|
|
436
436
|
# "Failed to fetch the current state of entities at Port. "
|
|
437
437
|
# "Skipping delete phase due to unknown initial state. "
|
|
438
438
|
# f"Error: {e}\n"
|
|
439
|
-
# f"Response status code: {e.
|
|
440
|
-
# f"Response content: {e.
|
|
439
|
+
# f"Response status code: {e.response.status_code if isinstance(e, httpx.HTTPStatusError) else None}\n"
|
|
440
|
+
# f"Response content: {e.response.text if isinstance(e, httpx.HTTPStatusError) else None}\n"
|
|
441
441
|
# )
|
|
442
442
|
# did_fetched_current_state = False
|
|
443
443
|
|
port_ocean/core/utils.py
CHANGED
|
@@ -39,10 +39,9 @@ async def validate_integration_runtime(
|
|
|
39
39
|
requested_runtime: Runtime,
|
|
40
40
|
) -> None:
|
|
41
41
|
logger.debug("Validating integration runtime")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
42
|
+
current_integration = await port_client.get_current_integration(
|
|
43
|
+
should_raise=False, should_log=False
|
|
44
|
+
)
|
|
46
45
|
current_runtime = current_integration.get("installationType", "OnPrem")
|
|
47
46
|
if current_integration and current_runtime != requested_runtime.value:
|
|
48
47
|
raise IntegrationRuntimeException(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Any, Callable, Type
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from port_ocean.helpers.retry import RetryTransport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OceanAsyncClient(httpx.AsyncClient):
|
|
10
|
+
"""
|
|
11
|
+
This class is a wrapper around httpx.AsyncClient that uses a custom transport class.
|
|
12
|
+
This is done to allow passing our custom transport class to the AsyncClient constructor while still allowing
|
|
13
|
+
all the default AsyncClient behavior that is changed when passing a custom transport instance.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
transport_class: Type[RetryTransport] = RetryTransport,
|
|
19
|
+
transport_kwargs: dict[str, Any] | None = None,
|
|
20
|
+
**kwargs: Any,
|
|
21
|
+
):
|
|
22
|
+
self._transport_kwargs = transport_kwargs
|
|
23
|
+
self._transport_class = transport_class
|
|
24
|
+
super().__init__(**kwargs)
|
|
25
|
+
|
|
26
|
+
def _init_transport( # type: ignore[override]
|
|
27
|
+
self,
|
|
28
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
29
|
+
app: Callable[..., Any] | None = None,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
) -> httpx.AsyncBaseTransport:
|
|
32
|
+
if transport is not None or app is not None:
|
|
33
|
+
return super()._init_transport(transport=transport, app=app, **kwargs)
|
|
34
|
+
|
|
35
|
+
return self._transport_class(
|
|
36
|
+
wrapped_transport=httpx.AsyncHTTPTransport(
|
|
37
|
+
**kwargs,
|
|
38
|
+
),
|
|
39
|
+
logger=logger,
|
|
40
|
+
**(self._transport_kwargs or {}),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _init_proxy_transport( # type: ignore[override]
|
|
44
|
+
self, proxy: httpx.Proxy, **kwargs: Any
|
|
45
|
+
) -> httpx.AsyncBaseTransport:
|
|
46
|
+
return self._transport_class(
|
|
47
|
+
wrapped_transport=httpx.AsyncHTTPTransport(
|
|
48
|
+
proxy=proxy,
|
|
49
|
+
**kwargs,
|
|
50
|
+
),
|
|
51
|
+
logger=logger,
|
|
52
|
+
**(self._transport_kwargs or {}),
|
|
53
|
+
)
|
port_ocean/helpers/retry.py
CHANGED
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import functools
|
|
3
2
|
import random
|
|
3
|
+
import time
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
from functools import partial
|
|
5
6
|
from http import HTTPStatus
|
|
6
|
-
from typing import Any, Callable, Coroutine, Iterable, Mapping
|
|
7
|
+
from typing import Any, Callable, Coroutine, Iterable, Mapping, Union
|
|
7
8
|
|
|
8
|
-
import
|
|
9
|
-
from aiohttp import ClientResponse
|
|
9
|
+
import httpx
|
|
10
10
|
from dateutil.parser import isoparse
|
|
11
|
-
from loguru import logger
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
# Adapted from https://github.com/encode/httpx/issues/108#issuecomment-
|
|
15
|
-
class
|
|
13
|
+
# Adapted from https://github.com/encode/httpx/issues/108#issuecomment-1434439481
|
|
14
|
+
class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
|
15
|
+
"""
|
|
16
|
+
A custom HTTP transport that automatically retries requests using an exponential backoff strategy
|
|
17
|
+
for specific HTTP status codes and request methods.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
wrapped_transport (Union[httpx.BaseTransport, httpx.AsyncBaseTransport]): The underlying HTTP transport
|
|
21
|
+
to wrap and use for making requests.
|
|
22
|
+
max_attempts (int, optional): The maximum number of times to retry a request before giving up. Defaults to 10.
|
|
23
|
+
max_backoff_wait (float, optional): The maximum time to wait between retries in seconds. Defaults to 60.
|
|
24
|
+
backoff_factor (float, optional): The factor by which the wait time increases with each retry attempt.
|
|
25
|
+
Defaults to 0.1.
|
|
26
|
+
jitter_ratio (float, optional): The amount of jitter to add to the backoff time. Jitter is a random
|
|
27
|
+
value added to the backoff time to avoid a "thundering herd" effect. The value should be between 0 and 0.5.
|
|
28
|
+
Defaults to 0.1.
|
|
29
|
+
respect_retry_after_header (bool, optional): Whether to respect the Retry-After header in HTTP responses
|
|
30
|
+
when deciding how long to wait before retrying. Defaults to True.
|
|
31
|
+
retryable_methods (Iterable[str], optional): The HTTP methods that can be retried. Defaults to
|
|
32
|
+
["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"].
|
|
33
|
+
retry_status_codes (Iterable[int], optional): The HTTP status codes that can be retried. Defaults to
|
|
34
|
+
[429, 502, 503, 504].
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
_wrapped_transport (Union[httpx.BaseTransport, httpx.AsyncBaseTransport]): The underlying HTTP transport
|
|
38
|
+
being wrapped.
|
|
39
|
+
_max_attempts (int): The maximum number of times to retry a request.
|
|
40
|
+
_backoff_factor (float): The factor by which the wait time increases with each retry attempt.
|
|
41
|
+
_respect_retry_after_header (bool): Whether to respect the Retry-After header in HTTP responses.
|
|
42
|
+
_retryable_methods (frozenset): The HTTP methods that can be retried.
|
|
43
|
+
_retry_status_codes (frozenset): The HTTP status codes that can be retried.
|
|
44
|
+
_jitter_ratio (float): The amount of jitter to add to the backoff time.
|
|
45
|
+
_max_backoff_wait (float): The maximum time to wait between retries in seconds.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
16
49
|
RETRYABLE_METHODS = frozenset(["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"])
|
|
17
50
|
RETRYABLE_STATUS_CODES = frozenset(
|
|
18
51
|
[
|
|
@@ -25,23 +58,23 @@ class RetryRequestClass(aiohttp.ClientRequest):
|
|
|
25
58
|
MAX_BACKOFF_WAIT = 60
|
|
26
59
|
|
|
27
60
|
def __init__(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*args,
|
|
39
|
-
**kwargs
|
|
61
|
+
self,
|
|
62
|
+
wrapped_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport],
|
|
63
|
+
max_attempts: int = 10,
|
|
64
|
+
max_backoff_wait: float = MAX_BACKOFF_WAIT,
|
|
65
|
+
backoff_factor: float = 0.1,
|
|
66
|
+
jitter_ratio: float = 0.1,
|
|
67
|
+
respect_retry_after_header: bool = True,
|
|
68
|
+
retryable_methods: Iterable[str] | None = None,
|
|
69
|
+
retry_status_codes: Iterable[int] | None = None,
|
|
70
|
+
logger: Any | None = None,
|
|
40
71
|
) -> None:
|
|
41
72
|
"""
|
|
42
73
|
Initializes the instance of RetryTransport class with the given parameters.
|
|
43
74
|
|
|
44
75
|
Args:
|
|
76
|
+
wrapped_transport (Union[httpx.BaseTransport, httpx.AsyncBaseTransport]):
|
|
77
|
+
The transport layer that will be wrapped and retried upon failure.
|
|
45
78
|
max_attempts (int, optional):
|
|
46
79
|
The maximum number of times the request can be retried in case of failure.
|
|
47
80
|
Defaults to 10.
|
|
@@ -65,6 +98,7 @@ class RetryRequestClass(aiohttp.ClientRequest):
|
|
|
65
98
|
Defaults to [429, 502, 503, 504].
|
|
66
99
|
logger (Any): The logger to use for logging retries.
|
|
67
100
|
"""
|
|
101
|
+
self._wrapped_transport = wrapped_transport
|
|
68
102
|
if jitter_ratio < 0 or jitter_ratio > 0.5:
|
|
69
103
|
raise ValueError(
|
|
70
104
|
f"Jitter ratio should be between 0 and 0.5, actual {jitter_ratio}"
|
|
@@ -85,34 +119,117 @@ class RetryRequestClass(aiohttp.ClientRequest):
|
|
|
85
119
|
)
|
|
86
120
|
self._jitter_ratio = jitter_ratio
|
|
87
121
|
self._max_backoff_wait = max_backoff_wait
|
|
122
|
+
self._logger = logger
|
|
123
|
+
|
|
124
|
+
def handle_request(self, request: httpx.Request) -> httpx.Response:
|
|
125
|
+
"""
|
|
126
|
+
Sends an HTTP request, possibly with retries.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
request (httpx.Request): The request to send.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
httpx.Response: The response received.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
transport: httpx.BaseTransport = self._wrapped_transport # type: ignore
|
|
136
|
+
if request.method in self._retryable_methods:
|
|
137
|
+
send_method = partial(transport.handle_request)
|
|
138
|
+
response = self._retry_operation(request, send_method)
|
|
139
|
+
else:
|
|
140
|
+
response = transport.handle_request(request)
|
|
141
|
+
return response
|
|
142
|
+
|
|
143
|
+
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
144
|
+
"""Sends an HTTP request, possibly with retries.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
request: The request to perform.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The response.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
transport: httpx.AsyncBaseTransport = self._wrapped_transport # type: ignore
|
|
154
|
+
if self._is_retryable_method(request):
|
|
155
|
+
send_method = partial(transport.handle_async_request)
|
|
156
|
+
response = await self._retry_operation_async(request, send_method)
|
|
157
|
+
else:
|
|
158
|
+
response = await transport.handle_async_request(request)
|
|
159
|
+
return response
|
|
160
|
+
|
|
161
|
+
async def aclose(self) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Closes the underlying HTTP transport, terminating all outstanding connections and rejecting any further
|
|
164
|
+
requests.
|
|
165
|
+
|
|
166
|
+
This should be called before the object is dereferenced, to ensure that connections are properly cleaned up.
|
|
167
|
+
"""
|
|
168
|
+
transport: httpx.AsyncBaseTransport = self._wrapped_transport # type: ignore
|
|
169
|
+
await transport.aclose()
|
|
88
170
|
|
|
89
|
-
|
|
171
|
+
def close(self) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Closes the underlying HTTP transport, terminating all outstanding connections and rejecting any further
|
|
174
|
+
requests.
|
|
90
175
|
|
|
91
|
-
|
|
92
|
-
|
|
176
|
+
This should be called before the object is dereferenced, to ensure that connections are properly cleaned up.
|
|
177
|
+
"""
|
|
178
|
+
transport: httpx.BaseTransport = self._wrapped_transport # type: ignore
|
|
179
|
+
transport.close()
|
|
180
|
+
|
|
181
|
+
def _is_retryable_method(self, request: httpx.Request) -> bool:
|
|
182
|
+
return request.method in self._retryable_methods or request.extensions.get(
|
|
183
|
+
"retryable", False
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _should_retry(self, response: httpx.Response) -> bool:
|
|
187
|
+
return response.status_code in self._retry_status_codes
|
|
93
188
|
|
|
94
189
|
def _log_error(
|
|
95
|
-
|
|
96
|
-
|
|
190
|
+
self,
|
|
191
|
+
request: httpx.Request,
|
|
192
|
+
error: Exception | None,
|
|
97
193
|
) -> None:
|
|
98
|
-
if
|
|
99
|
-
|
|
100
|
-
|
|
194
|
+
if not self._logger:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
if isinstance(error, httpx.ConnectTimeout):
|
|
198
|
+
self._logger.error(
|
|
199
|
+
f"Request {request.method} {request.url} failed to connect: {str(error)}"
|
|
101
200
|
)
|
|
102
|
-
elif isinstance(error,
|
|
103
|
-
|
|
104
|
-
f"Request {
|
|
201
|
+
elif isinstance(error, httpx.TimeoutException):
|
|
202
|
+
self._logger.error(
|
|
203
|
+
f"Request {request.method} {request.url} failed with a timeout exception: {str(error)}"
|
|
105
204
|
)
|
|
106
|
-
elif isinstance(error,
|
|
107
|
-
|
|
108
|
-
f"Request {
|
|
205
|
+
elif isinstance(error, httpx.HTTPError):
|
|
206
|
+
self._logger.error(
|
|
207
|
+
f"Request {request.method} {request.url} failed with an HTTP error: {str(error)}"
|
|
109
208
|
)
|
|
110
209
|
|
|
111
|
-
|
|
112
|
-
|
|
210
|
+
def _log_before_retry(
|
|
211
|
+
self,
|
|
212
|
+
request: httpx.Request,
|
|
213
|
+
sleep_time: float,
|
|
214
|
+
response: httpx.Response | None,
|
|
215
|
+
error: Exception | None,
|
|
216
|
+
) -> None:
|
|
217
|
+
if self._logger and response:
|
|
218
|
+
self._logger.warning(
|
|
219
|
+
f"Request {request.method} {request.url} failed with status code:"
|
|
220
|
+
f" {response.status_code}, retrying in {sleep_time} seconds." # noqa: F821
|
|
221
|
+
)
|
|
222
|
+
elif self._logger and error:
|
|
223
|
+
self._logger.warning(
|
|
224
|
+
f"Request {request.method} {request.url} failed with exception:"
|
|
225
|
+
f" {type(error).__name__} - {str(error) or 'No error message'}, retrying in {sleep_time} seconds."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
async def _should_retry_async(self, response: httpx.Response) -> bool:
|
|
229
|
+
return response.status_code in self._retry_status_codes
|
|
113
230
|
|
|
114
231
|
def _calculate_sleep(
|
|
115
|
-
|
|
232
|
+
self, attempts_made: int, headers: Union[httpx.Headers, Mapping[str, str]]
|
|
116
233
|
) -> float:
|
|
117
234
|
# Retry-After
|
|
118
235
|
# The Retry-After response HTTP header indicates how long the user agent should wait before
|
|
@@ -143,58 +260,91 @@ class RetryRequestClass(aiohttp.ClientRequest):
|
|
|
143
260
|
total_backoff = backoff + jitter
|
|
144
261
|
return min(total_backoff, self._max_backoff_wait)
|
|
145
262
|
|
|
146
|
-
def _log_before_retry(
|
|
147
|
-
self,
|
|
148
|
-
sleep_time: float,
|
|
149
|
-
response: ClientResponse | None,
|
|
150
|
-
error: Exception | None,
|
|
151
|
-
) -> None:
|
|
152
|
-
if response:
|
|
153
|
-
logger.warning(
|
|
154
|
-
f"Request {self.method} {self.url} failed with status code:"
|
|
155
|
-
f" {response.status}, retrying in {sleep_time} seconds." # noqa: F821
|
|
156
|
-
)
|
|
157
|
-
elif error:
|
|
158
|
-
logger.warning(
|
|
159
|
-
f"Request {self.method} {self.url} failed with exception:"
|
|
160
|
-
f" {type(error).__name__} - {str(error) or 'No error message'}, retrying in {sleep_time} seconds."
|
|
161
|
-
)
|
|
162
|
-
|
|
163
263
|
async def _retry_operation_async(
|
|
164
|
-
|
|
165
|
-
|
|
264
|
+
self,
|
|
265
|
+
request: httpx.Request,
|
|
266
|
+
send_method: Callable[..., Coroutine[Any, Any, httpx.Response]],
|
|
267
|
+
) -> httpx.Response:
|
|
166
268
|
remaining_attempts = self._max_attempts
|
|
167
269
|
attempts_made = 0
|
|
168
|
-
response:
|
|
270
|
+
response: httpx.Response | None = None
|
|
169
271
|
error: Exception | None = None
|
|
170
272
|
while True:
|
|
171
273
|
if attempts_made > 0:
|
|
172
|
-
sleep_time = self._calculate_sleep(attempts_made,
|
|
173
|
-
self._log_before_retry(sleep_time, response, error)
|
|
274
|
+
sleep_time = self._calculate_sleep(attempts_made, {})
|
|
275
|
+
self._log_before_retry(request, sleep_time, response, error)
|
|
174
276
|
await asyncio.sleep(sleep_time)
|
|
175
277
|
|
|
176
278
|
error = None
|
|
177
279
|
response = None
|
|
178
280
|
try:
|
|
179
|
-
response = await request
|
|
281
|
+
response = await send_method(request)
|
|
282
|
+
response.request = request
|
|
180
283
|
if remaining_attempts < 1 or not (
|
|
181
|
-
|
|
284
|
+
await self._should_retry_async(response)
|
|
182
285
|
):
|
|
183
286
|
return response
|
|
184
|
-
|
|
287
|
+
await response.aclose()
|
|
288
|
+
except httpx.ConnectTimeout as e:
|
|
185
289
|
error = e
|
|
186
290
|
if remaining_attempts < 1:
|
|
187
|
-
self._log_error(error)
|
|
291
|
+
self._log_error(request, error)
|
|
292
|
+
raise
|
|
293
|
+
except httpx.ReadTimeout as e:
|
|
294
|
+
error = e
|
|
295
|
+
if remaining_attempts < 1:
|
|
296
|
+
self._log_error(request, error)
|
|
297
|
+
raise
|
|
298
|
+
except httpx.TimeoutException as e:
|
|
299
|
+
error = e
|
|
300
|
+
if remaining_attempts < 1:
|
|
301
|
+
self._log_error(request, error)
|
|
302
|
+
raise
|
|
303
|
+
except httpx.HTTPError as e:
|
|
304
|
+
error = e
|
|
305
|
+
if remaining_attempts < 1:
|
|
306
|
+
self._log_error(request, error)
|
|
188
307
|
raise
|
|
189
308
|
attempts_made += 1
|
|
190
309
|
remaining_attempts -= 1
|
|
191
310
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
311
|
+
def _retry_operation(
|
|
312
|
+
self,
|
|
313
|
+
request: httpx.Request,
|
|
314
|
+
send_method: Callable[..., httpx.Response],
|
|
315
|
+
) -> httpx.Response:
|
|
316
|
+
remaining_attempts = self._max_attempts
|
|
317
|
+
attempts_made = 0
|
|
318
|
+
response: httpx.Response | None = None
|
|
319
|
+
error: Exception | None = None
|
|
320
|
+
while True:
|
|
321
|
+
if attempts_made > 0:
|
|
322
|
+
sleep_time = self._calculate_sleep(attempts_made, {})
|
|
323
|
+
self._log_before_retry(request, sleep_time, response, error)
|
|
324
|
+
time.sleep(sleep_time)
|
|
325
|
+
|
|
326
|
+
error = None
|
|
327
|
+
response = None
|
|
328
|
+
try:
|
|
329
|
+
response = send_method(request)
|
|
330
|
+
response.request = request
|
|
331
|
+
if remaining_attempts < 1 or not self._should_retry(response):
|
|
332
|
+
return response
|
|
333
|
+
response.close()
|
|
334
|
+
except httpx.ConnectTimeout as e:
|
|
335
|
+
error = e
|
|
336
|
+
if remaining_attempts < 1:
|
|
337
|
+
self._log_error(request, error)
|
|
338
|
+
raise
|
|
339
|
+
except httpx.TimeoutException as e:
|
|
340
|
+
error = e
|
|
341
|
+
if remaining_attempts < 1:
|
|
342
|
+
self._log_error(request, error)
|
|
343
|
+
raise
|
|
344
|
+
except httpx.HTTPError as e:
|
|
345
|
+
error = e
|
|
346
|
+
if remaining_attempts < 1:
|
|
347
|
+
self._log_error(request, error)
|
|
348
|
+
raise
|
|
349
|
+
attempts_made += 1
|
|
350
|
+
remaining_attempts -= 1
|