port-ocean 0.12.2.dev14__py3-none-any.whl → 0.12.2.dev17__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.

Files changed (29) hide show
  1. port_ocean/cli/commands/list_integrations.py +5 -8
  2. port_ocean/cli/commands/pull.py +16 -20
  3. port_ocean/clients/port/authentication.py +13 -12
  4. port_ocean/clients/port/client.py +8 -9
  5. port_ocean/clients/port/mixins/blueprints.py +14 -14
  6. port_ocean/clients/port/mixins/entities.py +8 -7
  7. port_ocean/clients/port/mixins/integrations.py +11 -11
  8. port_ocean/clients/port/mixins/migrations.py +5 -5
  9. port_ocean/clients/port/retry_transport.py +35 -13
  10. port_ocean/clients/port/utils.py +23 -32
  11. port_ocean/core/defaults/clean.py +3 -3
  12. port_ocean/core/defaults/common.py +3 -3
  13. port_ocean/core/defaults/initialize.py +47 -48
  14. port_ocean/core/integrations/mixins/sync_raw.py +3 -3
  15. port_ocean/core/utils.py +3 -4
  16. port_ocean/helpers/async_client.py +53 -0
  17. port_ocean/helpers/retry.py +221 -71
  18. port_ocean/ocean.py +22 -20
  19. port_ocean/run.py +3 -3
  20. port_ocean/tests/clients/port/mixins/test_entities.py +4 -3
  21. port_ocean/tests/test_smoke.py +3 -3
  22. port_ocean/utils/async_http.py +8 -13
  23. port_ocean/utils/repeat.py +2 -6
  24. port_ocean/utils/signal.py +2 -1
  25. {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev17.dist-info}/METADATA +1 -2
  26. {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev17.dist-info}/RECORD +29 -28
  27. {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev17.dist-info}/LICENSE.md +0 -0
  28. {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev17.dist-info}/WHEEL +0 -0
  29. {port_ocean-0.12.2.dev14.dist-info → port_ocean-0.12.2.dev17.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 aiohttp
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
- raw_blueprints: list[dict[str, Any]],
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
- port_client: PortClient,
57
- default_mapping: PortAppConfig,
58
- integration_config: IntegrationConfiguration,
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 aiohttp.ClientResponseError as err:
84
- logger.error(f"Failed to apply default mapping: {err.message}.")
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
- integration.get("changelogDestination") != changelog_destination
93
- or integration.get("installationAppType") != integration_config.integration.type
94
- or integration.get("version") != port_client.integration_version
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
- port_client: PortClient,
103
- defaults: Defaults,
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, aiohttp.ClientResponseError):
136
+ if isinstance(error, httpx.HTTPStatusError):
137
137
  logger.warning(
138
- f"Failed to create resources: {error.message}. Rolling back changes..."
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 aiohttp.ClientResponseError as err:
159
- logger.error(f"Failed to create resources: {err.message}. continuing...")
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, aiohttp.ClientResponseError):
188
+ if isinstance(error, httpx.HTTPStatusError):
189
189
  logger.warning(
190
- f"Failed to create resource: {error.message}. continuing..."
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
- config_class: Type[PortAppConfig], integration_config: IntegrationConfiguration
198
+ config_class: Type[PortAppConfig], integration_config: IntegrationConfiguration
199
199
  ) -> None:
200
200
  port_client = ocean.port_client
201
- async with ocean.port_client.client:
202
- defaults = get_port_integration_defaults(config_class)
203
- if not defaults:
204
- logger.warning("No defaults found. Skipping initialization...")
205
- return None
206
-
207
- if defaults.port_app_config:
208
- await _initialize_required_integration_settings(
209
- port_client, defaults.port_app_config, integration_config
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
- if not integration_config.initialize_port_resources:
213
- return
211
+ if not integration_config.initialize_port_resources:
212
+ return
214
213
 
215
- try:
216
- logger.info("Found default resources, starting creation process")
217
- await _create_resources(port_client, defaults)
218
- except AbortDefaultCreationError as e:
219
- logger.warning(
220
- f"Failed to create resources. Rolling back blueprints : {e.blueprints_to_rollback}"
221
- )
222
- await asyncio.gather(
223
- *(
224
- port_client.delete_blueprint(
225
- identifier,
226
- should_raise=False,
227
- user_agent_type=UserAgentType.exporter,
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
- raise ExceptionGroup(str(e), e.errors)
230
+ )
231
+ raise ExceptionGroup(str(e), e.errors)
233
232
 
234
233
 
235
234
  def initialize_defaults(
236
- config_class: Type[PortAppConfig], integration_config: IntegrationConfiguration
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 aiohttp.ClientError as e:
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.status if isinstance(e, aiohttp.ClientResponseError) else None}\n"
440
- # f"Response content: {e.message if isinstance(e, aiohttp.ClientResponseError) else None}\n"
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
- async with port_client.client:
43
- current_integration = await port_client.get_current_integration(
44
- should_raise=False, should_log=False
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
+ )
@@ -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 aiohttp
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-1434439481n
15
- class RetryRequestClass(aiohttp.ClientRequest):
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
- self,
29
- method,
30
- url,
31
- max_attempts: int = 10,
32
- max_backoff_wait: float = MAX_BACKOFF_WAIT,
33
- backoff_factor: float = 0.1,
34
- jitter_ratio: float = 0.1,
35
- respect_retry_after_header: bool = True,
36
- retryable_methods: Iterable[str] | None = None,
37
- retry_status_codes: Iterable[int] | None = None,
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
- super().__init__(method, url, *args, **kwargs)
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
- def _is_retryable(self) -> bool:
92
- return self.method in self._retryable_methods
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
- self,
96
- error: Exception | None,
190
+ self,
191
+ request: httpx.Request,
192
+ error: Exception | None,
97
193
  ) -> None:
98
- if isinstance(error, aiohttp.ServerConnectionError):
99
- logger.error(
100
- f"Request {self.method} {self.url} failed to connect: {str(error)}"
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, aiohttp.ConnectionTimeoutError):
103
- logger.error(
104
- f"Request {self.method} {self.url} failed with a timeout exception: {str(error)}"
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, aiohttp.ClientError):
107
- logger.error(
108
- f"Request {self.method} {self.url} failed with an HTTP error: {str(error)}"
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
- async def _should_retry_async(self, response: ClientResponse) -> bool:
112
- return response.status in self._retry_status_codes
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
- self, attempts_made: int, headers: Mapping[str, str]
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
- self, request: Callable[..., Coroutine[Any, Any, ClientResponse]]
165
- ) -> ClientResponse:
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: ClientResponse | None = None
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, response.headers if response else {})
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
- await self._should_retry_async(response)
284
+ await self._should_retry_async(response)
182
285
  ):
183
286
  return response
184
- except (aiohttp.ServerConnectionError, aiohttp.ConnectionTimeoutError, aiohttp.ClientError) as e:
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
- async def send(self, conn: "Connection") -> "ClientResponse":
193
- request = functools.partial(
194
- super().send, conn
195
- )
196
- if self._is_retryable():
197
- response = await self._retry_operation_async(request)
198
- else:
199
- response = await request()
200
- return response
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