port-ocean 0.27.9__py3-none-any.whl → 0.28.0__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.
@@ -35,6 +35,7 @@ class PortAuthentication:
35
35
  integration_identifier: str,
36
36
  integration_type: str,
37
37
  integration_version: str,
38
+ ingest_url: str,
38
39
  ):
39
40
  self.client = client
40
41
  self.api_url = api_url
@@ -43,6 +44,7 @@ class PortAuthentication:
43
44
  self.integration_identifier = integration_identifier
44
45
  self.integration_type = integration_type
45
46
  self.integration_version = integration_version
47
+ self.ingest_url = ingest_url
46
48
  self.last_token_object: TokenResponse | None = None
47
49
 
48
50
  async def _get_token(self, client_id: str, client_secret: str) -> TokenResponse:
@@ -1,3 +1,5 @@
1
+ from typing import Any
2
+
1
3
  from loguru import logger
2
4
 
3
5
  from port_ocean.clients.port.authentication import PortAuthentication
@@ -10,11 +12,10 @@ from port_ocean.clients.port.types import (
10
12
  KafkaCreds,
11
13
  )
12
14
  from port_ocean.clients.port.utils import (
13
- handle_port_status_code,
14
15
  get_internal_http_client,
16
+ handle_port_status_code,
15
17
  )
16
18
  from port_ocean.exceptions.clients import KafkaCredentialsNotFound
17
- from typing import Any
18
19
 
19
20
 
20
21
  class PortClient(
@@ -32,6 +33,7 @@ class PortClient(
32
33
  integration_identifier: str,
33
34
  integration_type: str,
34
35
  integration_version: str,
36
+ ingest_url: str,
35
37
  ):
36
38
  self.api_url = f"{base_url}/v1"
37
39
  self.client = get_internal_http_client(self)
@@ -43,6 +45,7 @@ class PortClient(
43
45
  integration_identifier,
44
46
  integration_type,
45
47
  integration_version,
48
+ ingest_url,
46
49
  )
47
50
  EntityClientMixin.__init__(self, self.auth, self.client)
48
51
  IntegrationClientMixin.__init__(
@@ -296,7 +296,7 @@ class IntegrationClientMixin:
296
296
  logger.debug("starting POST raw data request", raw_data=raw_data)
297
297
  headers = await self.auth.headers()
298
298
  response = await self.client.post(
299
- f"{self.auth.api_url}/lakehouse/integration/{self.integration_identifier}/sync/{sync_id}/kind/{kind}/items",
299
+ f"{self.auth.ingest_url}/lakehouse/integration-type/{self.auth.integration_type}/integration/{self.integration_identifier}/sync/{sync_id}/kind/{kind}/items",
300
300
  headers=headers,
301
301
  json={
302
302
  "items": raw_data,
@@ -9,7 +9,6 @@ from pydantic.main import BaseModel
9
9
 
10
10
  from port_ocean.config.base import BaseOceanModel, BaseOceanSettings
11
11
  from port_ocean.core.event_listener import EventListenerSettingsType
12
-
13
12
  from port_ocean.core.models import (
14
13
  CachingStorageMode,
15
14
  CreatePortResourcesOrigin,
@@ -47,6 +46,7 @@ class PortSettings(BaseOceanModel, extra=Extra.allow):
47
46
  client_secret: str = Field(..., sensitive=True)
48
47
  base_url: AnyHttpUrl = parse_obj_as(AnyHttpUrl, "https://api.getport.io")
49
48
  port_app_config_cache_ttl: int = 60
49
+ ingest_url: AnyHttpUrl = parse_obj_as(AnyHttpUrl, "https://ingest.getport.io")
50
50
 
51
51
 
52
52
  class IntegrationSettings(BaseOceanModel, extra=Extra.allow):
@@ -3,7 +3,7 @@ from typing import Any, Type
3
3
  import httpx
4
4
  from loguru import logger
5
5
 
6
- from port_ocean.helpers.retry import RetryTransport
6
+ from port_ocean.helpers.retry import RetryTransport, RetryConfig
7
7
  from port_ocean.helpers.stream import Stream
8
8
 
9
9
 
@@ -18,10 +18,12 @@ class OceanAsyncClient(httpx.AsyncClient):
18
18
  self,
19
19
  transport_class: Type[RetryTransport] = RetryTransport,
20
20
  transport_kwargs: dict[str, Any] | None = None,
21
+ retry_config: RetryConfig | None = None,
21
22
  **kwargs: Any,
22
23
  ):
23
24
  self._transport_kwargs = transport_kwargs
24
25
  self._transport_class = transport_class
26
+ self._retry_config = retry_config
25
27
  super().__init__(**kwargs)
26
28
 
27
29
  def _init_transport( # type: ignore[override]
@@ -33,9 +35,8 @@ class OceanAsyncClient(httpx.AsyncClient):
33
35
  return super()._init_transport(transport=transport, **kwargs)
34
36
 
35
37
  return self._transport_class(
36
- wrapped_transport=httpx.AsyncHTTPTransport(
37
- **kwargs,
38
- ),
38
+ wrapped_transport=httpx.AsyncHTTPTransport(**kwargs),
39
+ retry_config=self._retry_config,
39
40
  logger=logger,
40
41
  **(self._transport_kwargs or {}),
41
42
  )
@@ -44,10 +45,8 @@ class OceanAsyncClient(httpx.AsyncClient):
44
45
  self, proxy: httpx.Proxy, **kwargs: Any
45
46
  ) -> httpx.AsyncBaseTransport:
46
47
  return self._transport_class(
47
- wrapped_transport=httpx.AsyncHTTPTransport(
48
- proxy=proxy,
49
- **kwargs,
50
- ),
48
+ wrapped_transport=httpx.AsyncHTTPTransport(proxy=proxy, **kwargs),
49
+ retry_config=self._retry_config,
51
50
  logger=logger,
52
51
  **(self._transport_kwargs or {}),
53
52
  )
@@ -4,12 +4,24 @@ import time
4
4
  from datetime import datetime
5
5
  from functools import partial
6
6
  from http import HTTPStatus
7
- from typing import Any, Callable, Coroutine, Iterable, Mapping, Union, cast
7
+ from typing import (
8
+ Any,
9
+ Callable,
10
+ Coroutine,
11
+ Iterable,
12
+ Mapping,
13
+ Union,
14
+ cast,
15
+ Optional,
16
+ List,
17
+ )
8
18
  import httpx
9
19
  from dateutil.parser import isoparse
10
20
  import logging
11
21
 
22
+ MAX_BACKOFF_WAIT_IN_SECONDS = 60
12
23
  _ON_RETRY_CALLBACK: Callable[[httpx.Request], httpx.Request] | None = None
24
+ _RETRY_CONFIG_CALLBACK: Callable[[], "RetryConfig"] | None = None
13
25
 
14
26
 
15
27
  def register_on_retry_callback(
@@ -19,6 +31,92 @@ def register_on_retry_callback(
19
31
  _ON_RETRY_CALLBACK = _on_retry_callback
20
32
 
21
33
 
34
+ def register_retry_config_callback(
35
+ retry_config_callback: Callable[[], "RetryConfig"]
36
+ ) -> None:
37
+ """Register a callback function that returns a RetryConfig instance.
38
+
39
+ The callback will be called when a RetryTransport needs to be created.
40
+
41
+ Args:
42
+ retry_config_callback: A function that returns a RetryConfig instance
43
+ """
44
+ global _RETRY_CONFIG_CALLBACK
45
+ _RETRY_CONFIG_CALLBACK = retry_config_callback
46
+
47
+
48
+ class RetryConfig:
49
+ """Configuration class for retry behavior that can be customized per integration."""
50
+
51
+ def __init__(
52
+ self,
53
+ max_attempts: int = 10,
54
+ max_backoff_wait: float = MAX_BACKOFF_WAIT_IN_SECONDS,
55
+ base_delay: float = 0.1,
56
+ jitter_ratio: float = 0.1,
57
+ respect_retry_after_header: bool = True,
58
+ retryable_methods: Optional[Iterable[str]] = None,
59
+ retry_status_codes: Optional[Iterable[int]] = None,
60
+ retry_after_headers: Optional[List[str]] = None,
61
+ additional_retry_status_codes: Optional[Iterable[int]] = None,
62
+ ):
63
+ """
64
+ Initialize retry configuration.
65
+
66
+ Args:
67
+ max_attempts: Maximum number of retry attempts
68
+ max_backoff_wait: Maximum backoff wait time in seconds
69
+ base_delay: Base delay for exponential backoff
70
+ jitter_ratio: Jitter ratio for backoff (0-0.5)
71
+ respect_retry_after_header: Whether to respect Retry-After header
72
+ retryable_methods: HTTP methods that can be retried (overrides defaults if provided)
73
+ retry_status_codes: DEPRECATED - use additional_retry_status_codes instead
74
+ retry_after_headers: Custom headers to check for retry timing (e.g., ['X-RateLimit-Reset', 'Retry-After'])
75
+ additional_retry_status_codes: Additional status codes to retry (extends system defaults)
76
+ """
77
+ self.max_attempts = max_attempts
78
+ self.max_backoff_wait = max_backoff_wait
79
+ self.base_delay = base_delay
80
+ self.jitter_ratio = jitter_ratio
81
+ self.respect_retry_after_header = respect_retry_after_header
82
+
83
+ # Default retryable methods - always include these unless explicitly overridden
84
+ default_methods = frozenset(
85
+ ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
86
+ )
87
+ self.retryable_methods = (
88
+ frozenset(retryable_methods) if retryable_methods else default_methods
89
+ )
90
+
91
+ # Default retry status codes - always include these for system reliability
92
+ default_status_codes = frozenset(
93
+ [
94
+ HTTPStatus.TOO_MANY_REQUESTS,
95
+ HTTPStatus.BAD_GATEWAY,
96
+ HTTPStatus.SERVICE_UNAVAILABLE,
97
+ HTTPStatus.GATEWAY_TIMEOUT,
98
+ HTTPStatus.UNAUTHORIZED,
99
+ HTTPStatus.BAD_REQUEST,
100
+ ]
101
+ )
102
+
103
+ # Additional status codes to retry (extends defaults)
104
+ additional_codes = (
105
+ frozenset(additional_retry_status_codes)
106
+ if additional_retry_status_codes
107
+ else frozenset()
108
+ )
109
+
110
+ # Combine defaults with additional codes for extensibility
111
+ self.retry_status_codes = default_status_codes | additional_codes
112
+ self.retry_after_headers = retry_after_headers or ["Retry-After"]
113
+
114
+ if jitter_ratio < 0 or jitter_ratio > 0.5:
115
+ raise ValueError(
116
+ f"Jitter ratio should be between 0 and 0.5, actual {jitter_ratio}"
117
+ )
118
+
119
+
22
120
  # Adapted from https://github.com/encode/httpx/issues/108#issuecomment-1434439481
23
121
  class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
24
122
  """
@@ -41,32 +139,16 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
41
139
  ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"].
42
140
  retry_status_codes (Iterable[int], optional): The HTTP status codes that can be retried. Defaults to
43
141
  [429, 502, 503, 504].
142
+ retry_config (RetryConfig, optional): Configuration for retry behavior. If not provided, uses defaults.
143
+ logger (Any, optional): The logger to use for logging retries.
44
144
 
45
145
  Attributes:
46
146
  _wrapped_transport (Union[httpx.BaseTransport, httpx.AsyncBaseTransport]): The underlying HTTP transport
47
147
  being wrapped.
48
- _max_attempts (int): The maximum number of times to retry a request.
49
- _backoff_factor (float): The factor by which the wait time increases with each retry attempt.
50
- _respect_retry_after_header (bool): Whether to respect the Retry-After header in HTTP responses.
51
- _retryable_methods (frozenset): The HTTP methods that can be retried.
52
- _retry_status_codes (frozenset): The HTTP status codes that can be retried.
53
- _jitter_ratio (float): The amount of jitter to add to the backoff time.
54
- _max_backoff_wait (float): The maximum time to wait between retries in seconds.
148
+ _retry_config (RetryConfig): The retry configuration object.
149
+ _logger (Any): The logger to use for logging retries.
55
150
  """
56
151
 
57
- RETRYABLE_METHODS = frozenset(["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"])
58
- RETRYABLE_STATUS_CODES = frozenset(
59
- [
60
- HTTPStatus.TOO_MANY_REQUESTS,
61
- HTTPStatus.BAD_GATEWAY,
62
- HTTPStatus.SERVICE_UNAVAILABLE,
63
- HTTPStatus.GATEWAY_TIMEOUT,
64
- HTTPStatus.UNAUTHORIZED,
65
- HTTPStatus.BAD_REQUEST,
66
- ]
67
- )
68
- MAX_BACKOFF_WAIT_IN_SECONDS = 60
69
-
70
152
  def __init__(
71
153
  self,
72
154
  wrapped_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport],
@@ -77,6 +159,7 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
77
159
  respect_retry_after_header: bool = True,
78
160
  retryable_methods: Iterable[str] | None = None,
79
161
  retry_status_codes: Iterable[int] | None = None,
162
+ retry_config: Optional[RetryConfig] = None,
80
163
  logger: Any | None = None,
81
164
  ) -> None:
82
165
  """
@@ -106,29 +189,27 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
106
189
  retry_status_codes (Iterable[int], optional):
107
190
  The HTTP status codes that can be retried.
108
191
  Defaults to [429, 502, 503, 504].
192
+ retry_config (RetryConfig, optional):
193
+ Configuration for retry behavior. If not provided, uses default configuration.
109
194
  logger (Any): The logger to use for logging retries.
110
195
  """
111
196
  self._wrapped_transport = wrapped_transport
112
- if jitter_ratio < 0 or jitter_ratio > 0.5:
113
- raise ValueError(
114
- f"Jitter ratio should be between 0 and 0.5, actual {jitter_ratio}"
197
+
198
+ if retry_config is not None:
199
+ self._retry_config = retry_config
200
+ elif _RETRY_CONFIG_CALLBACK is not None:
201
+ self._retry_config = _RETRY_CONFIG_CALLBACK()
202
+ else:
203
+ self._retry_config = RetryConfig(
204
+ max_attempts=max_attempts,
205
+ max_backoff_wait=max_backoff_wait,
206
+ base_delay=base_delay,
207
+ jitter_ratio=jitter_ratio,
208
+ respect_retry_after_header=respect_retry_after_header,
209
+ retryable_methods=retryable_methods,
210
+ retry_status_codes=retry_status_codes,
115
211
  )
116
212
 
117
- self._max_attempts = max_attempts
118
- self._base_delay = base_delay
119
- self._respect_retry_after_header = respect_retry_after_header
120
- self._retryable_methods = (
121
- frozenset(retryable_methods)
122
- if retryable_methods
123
- else self.RETRYABLE_METHODS
124
- )
125
- self._retry_status_codes = (
126
- frozenset(retry_status_codes)
127
- if retry_status_codes
128
- else self.RETRYABLE_STATUS_CODES
129
- )
130
- self._jitter_ratio = jitter_ratio
131
- self._max_backoff_wait = max_backoff_wait
132
213
  self._logger = logger
133
214
 
134
215
  def handle_request(self, request: httpx.Request) -> httpx.Response:
@@ -206,12 +287,13 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
206
287
  transport.close()
207
288
 
208
289
  def _is_retryable_method(self, request: httpx.Request) -> bool:
209
- return request.method in self._retryable_methods or request.extensions.get(
210
- "retryable", False
290
+ return (
291
+ request.method in self._retry_config.retryable_methods
292
+ or request.extensions.get("retryable", False)
211
293
  )
212
294
 
213
295
  def _should_retry(self, response: httpx.Response) -> bool:
214
- return response.status_code in self._retry_status_codes
296
+ return response.status_code in self._retry_config.retry_status_codes
215
297
 
216
298
  def _log_error(
217
299
  self,
@@ -314,46 +396,54 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
314
396
  )
315
397
 
316
398
  async def _should_retry_async(self, response: httpx.Response) -> bool:
317
- return response.status_code in self._retry_status_codes
399
+ return response.status_code in self._retry_config.retry_status_codes
318
400
 
319
401
  def _calculate_sleep(
320
402
  self, attempts_made: int, headers: Union[httpx.Headers, Mapping[str, str]]
321
403
  ) -> float:
322
- # Retry-After
323
- # The Retry-After response HTTP header indicates how long the user agent should wait before
324
- # making a follow-up request. There are three main cases this header is used:
325
- # - When sent with a 503 (Service Unavailable) response, this indicates how long the service
326
- # is expected to be unavailable.
327
- # - When sent with a 429 (Too Many Requests) response, this indicates how long to wait before
328
- # making a new request.
329
- # - When sent with a redirect response, such as 301 (Moved Permanently), this indicates the
330
- # minimum time that the user agent is asked to wait before issuing the redirected request.
331
- retry_after_header = (headers.get("Retry-After") or "").strip()
332
- if self._respect_retry_after_header and retry_after_header:
333
- if retry_after_header.isdigit():
334
- return float(retry_after_header)
335
-
336
- try:
337
- parsed_date = isoparse(
338
- retry_after_header
339
- ).astimezone() # converts to local time
340
- diff = (parsed_date - datetime.now().astimezone()).total_seconds()
341
- if diff > 0:
342
- return min(diff, self._max_backoff_wait)
343
- except ValueError:
344
- pass
345
-
346
- backoff = self._base_delay * (2 ** (attempts_made - 1))
347
- jitter = (backoff * self._jitter_ratio) * random.choice([1, -1])
404
+ # Check custom retry headers first, then fall back to Retry-After
405
+ if self._retry_config.respect_retry_after_header:
406
+ for header_name in self._retry_config.retry_after_headers:
407
+ if header_value := (headers.get(header_name) or "").strip():
408
+ sleep_time = self._parse_retry_header(header_value)
409
+ if sleep_time is not None:
410
+ return min(sleep_time, self._retry_config.max_backoff_wait)
411
+
412
+ # Fall back to exponential backoff
413
+ backoff = self._retry_config.base_delay * (2 ** (attempts_made - 1))
414
+ jitter = (backoff * self._retry_config.jitter_ratio) * random.choice([1, -1])
348
415
  total_backoff = backoff + jitter
349
- return min(total_backoff, self._max_backoff_wait)
416
+ return min(total_backoff, self._retry_config.max_backoff_wait)
417
+
418
+ def _parse_retry_header(self, header_value: str) -> Optional[float]:
419
+ """Parse retry header value and return sleep time in seconds.
420
+
421
+ Args:
422
+ header_value: The header value to parse (e.g., "30", "2023-12-01T12:00:00Z")
423
+
424
+ Returns:
425
+ Sleep time in seconds if parsing succeeds, None if the header value cannot be parsed
426
+ """
427
+ if header_value.isdigit():
428
+ return float(header_value)
429
+
430
+ try:
431
+ # Try to parse as ISO date (common for rate limit headers like X-RateLimit-Reset)
432
+ parsed_date = isoparse(header_value).astimezone()
433
+ diff = (parsed_date - datetime.now().astimezone()).total_seconds()
434
+ if diff > 0:
435
+ return diff
436
+ except ValueError:
437
+ pass
438
+
439
+ return None
350
440
 
351
441
  async def _retry_operation_async(
352
442
  self,
353
443
  request: httpx.Request,
354
444
  send_method: Callable[..., Coroutine[Any, Any, httpx.Response]],
355
445
  ) -> httpx.Response:
356
- remaining_attempts = self._max_attempts
446
+ remaining_attempts = self._retry_config.max_attempts
357
447
  attempts_made = 0
358
448
  response: httpx.Response | None = None
359
449
  error: Exception | None = None
@@ -403,7 +493,7 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
403
493
  request: httpx.Request,
404
494
  send_method: Callable[..., httpx.Response],
405
495
  ) -> httpx.Response:
406
- remaining_attempts = self._max_attempts
496
+ remaining_attempts = self._retry_config.max_attempts
407
497
  attempts_made = 0
408
498
  response: httpx.Response | None = None
409
499
  error: Exception | None = None
port_ocean/ocean.py CHANGED
@@ -1,21 +1,18 @@
1
1
  import asyncio
2
2
  import sys
3
- from contextlib import asynccontextmanager
4
3
  import threading
4
+ from contextlib import asynccontextmanager
5
5
  from typing import Any, AsyncIterator, Callable, Dict, Type
6
6
 
7
- from port_ocean.cache.base import CacheProvider
8
- from port_ocean.cache.disk import DiskCacheProvider
9
- from port_ocean.cache.memory import InMemoryCacheProvider
10
- from port_ocean.core.models import ProcessExecutionMode
11
- import port_ocean.helpers.metric.metric
12
-
13
- from fastapi import FastAPI, APIRouter
14
-
7
+ from fastapi import APIRouter, FastAPI
15
8
  from loguru import logger
16
9
  from pydantic import BaseModel
17
10
  from starlette.types import Receive, Scope, Send
18
11
 
12
+ import port_ocean.helpers.metric.metric
13
+ from port_ocean.cache.base import CacheProvider
14
+ from port_ocean.cache.disk import DiskCacheProvider
15
+ from port_ocean.cache.memory import InMemoryCacheProvider
19
16
  from port_ocean.clients.port.client import PortClient
20
17
  from port_ocean.config.settings import (
21
18
  IntegrationConfiguration,
@@ -26,16 +23,17 @@ from port_ocean.context.ocean import (
26
23
  ocean,
27
24
  )
28
25
  from port_ocean.core.handlers.resync_state_updater import ResyncStateUpdater
26
+ from port_ocean.core.handlers.webhook.processor_manager import (
27
+ LiveEventsProcessorManager,
28
+ )
29
29
  from port_ocean.core.integrations.base import BaseIntegration
30
+ from port_ocean.core.models import ProcessExecutionMode
30
31
  from port_ocean.log.sensetive import sensitive_log_filter
31
32
  from port_ocean.middlewares import request_handler
32
33
  from port_ocean.utils.misc import IntegrationStateStatus
33
34
  from port_ocean.utils.repeat import repeat_every
34
35
  from port_ocean.utils.signal import signal_handler
35
36
  from port_ocean.version import __integration_version__
36
- from port_ocean.core.handlers.webhook.processor_manager import (
37
- LiveEventsProcessorManager,
38
- )
39
37
 
40
38
 
41
39
  class Ocean:
@@ -69,6 +67,7 @@ class Ocean:
69
67
  integration_identifier=self.config.integration.identifier,
70
68
  integration_type=self.config.integration.type,
71
69
  integration_version=__integration_version__,
70
+ ingest_url=self.config.port.ingest_url,
72
71
  )
73
72
  self.cache_provider: CacheProvider = self._get_caching_provider()
74
73
  self.process_execution_mode: ProcessExecutionMode = (
@@ -96,7 +96,13 @@ def mock_http_client() -> MagicMock:
96
96
  @pytest.fixture
97
97
  def mock_port_client(mock_http_client: MagicMock) -> PortClient:
98
98
  mock_port_client = PortClient(
99
- MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
99
+ MagicMock(),
100
+ MagicMock(),
101
+ MagicMock(),
102
+ MagicMock(),
103
+ MagicMock(),
104
+ MagicMock(),
105
+ MagicMock(),
100
106
  )
101
107
  mock_port_client.auth = AsyncMock()
102
108
  mock_port_client.auth.headers = AsyncMock(
@@ -1,7 +1,9 @@
1
1
  from typing import Any
2
- from httpx import Response
3
- import pytest
4
2
  from unittest.mock import AsyncMock, MagicMock, patch
3
+
4
+ import pytest
5
+ from httpx import Response
6
+
5
7
  from port_ocean.clients.port.client import PortClient
6
8
  from port_ocean.clients.port.types import UserAgentType
7
9
  from port_ocean.context.ocean import PortOceanContext
@@ -11,8 +13,6 @@ from port_ocean.core.handlers.entities_state_applier.port.applier import (
11
13
  from port_ocean.core.handlers.entity_processor.jq_entity_processor import (
12
14
  JQEntityProcessor,
13
15
  )
14
- from port_ocean.core.handlers.webhook.webhook_event import WebhookEventRawResults
15
- from port_ocean.core.integrations.mixins.live_events import LiveEventsMixin
16
16
  from port_ocean.core.handlers.port_app_config.models import (
17
17
  EntityMapping,
18
18
  MappingsConfig,
@@ -21,6 +21,8 @@ from port_ocean.core.handlers.port_app_config.models import (
21
21
  ResourceConfig,
22
22
  Selector,
23
23
  )
24
+ from port_ocean.core.handlers.webhook.webhook_event import WebhookEventRawResults
25
+ from port_ocean.core.integrations.mixins.live_events import LiveEventsMixin
24
26
  from port_ocean.core.models import Entity
25
27
  from port_ocean.core.ocean_types import CalculationResult, EntitySelectorDiff
26
28
  from port_ocean.ocean import Ocean
@@ -253,7 +255,13 @@ def mock_port_app_config_handler(
253
255
  @pytest.fixture
254
256
  def mock_port_client(mock_http_client: MagicMock) -> PortClient:
255
257
  mock_port_client = PortClient(
256
- MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
258
+ MagicMock(),
259
+ MagicMock(),
260
+ MagicMock(),
261
+ MagicMock(),
262
+ MagicMock(),
263
+ MagicMock(),
264
+ MagicMock(),
257
265
  )
258
266
  mock_port_client.auth = AsyncMock()
259
267
  mock_port_client.auth.headers = AsyncMock(
@@ -340,10 +348,11 @@ async def test_parse_raw_event_results_to_entities_creation(
340
348
  calculation_result
341
349
  )
342
350
 
343
- entities_to_create, entities_to_delete = (
344
- await mock_live_events_mixin._parse_raw_event_results_to_entities(
345
- [one_webhook_event_raw_results_for_creation]
346
- )
351
+ (
352
+ entities_to_create,
353
+ entities_to_delete,
354
+ ) = await mock_live_events_mixin._parse_raw_event_results_to_entities(
355
+ [one_webhook_event_raw_results_for_creation]
347
356
  )
348
357
 
349
358
  assert entities_to_create == [entity]
@@ -367,10 +376,11 @@ async def test_parse_raw_event_results_to_entities_deletion(
367
376
  calculation_result
368
377
  )
369
378
 
370
- entities_to_create, entities_to_delete = (
371
- await mock_live_events_mixin._parse_raw_event_results_to_entities(
372
- [one_webhook_event_raw_results_for_deletion]
373
- )
379
+ (
380
+ entities_to_create,
381
+ entities_to_delete,
382
+ ) = await mock_live_events_mixin._parse_raw_event_results_to_entities(
383
+ [one_webhook_event_raw_results_for_deletion]
374
384
  )
375
385
 
376
386
  assert entities_to_create == []
@@ -1,32 +1,16 @@
1
- import pytest
2
- from port_ocean.core.handlers.webhook.processor_manager import (
3
- LiveEventsProcessorManager,
4
- )
5
- from port_ocean.core.handlers.webhook.abstract_webhook_processor import (
6
- AbstractWebhookProcessor,
7
- )
8
- from port_ocean.core.handlers.webhook.webhook_event import (
9
- EventHeaders,
10
- WebhookEvent,
11
- WebhookEventRawResults,
12
- EventPayload,
13
- )
14
- from fastapi import APIRouter
15
- from port_ocean.core.integrations.mixins.handler import HandlerMixin
16
- from port_ocean.utils.signal import SignalHandler
17
- from typing import Dict, Any
18
1
  import asyncio
2
+ from typing import Any, Dict
3
+ from unittest.mock import AsyncMock, MagicMock, patch
4
+
5
+ import pytest
6
+ from fastapi import APIRouter, FastAPI
19
7
  from fastapi.testclient import TestClient
20
- from fastapi import FastAPI
21
- from port_ocean.context.ocean import PortOceanContext
22
- from unittest.mock import AsyncMock
23
- from port_ocean.context.event import EventContext, event_context, EventType
24
- from port_ocean.context.ocean import ocean
25
- from unittest.mock import MagicMock, patch
26
8
  from httpx import Response
27
- from port_ocean.clients.port.client import PortClient
9
+
28
10
  from port_ocean import Ocean
29
- from port_ocean.core.integrations.base import BaseIntegration
11
+ from port_ocean.clients.port.client import PortClient
12
+ from port_ocean.context.event import EventContext, EventType, event_context
13
+ from port_ocean.context.ocean import PortOceanContext, ocean
30
14
  from port_ocean.core.handlers.port_app_config.models import (
31
15
  EntityMapping,
32
16
  MappingsConfig,
@@ -35,13 +19,28 @@ from port_ocean.core.handlers.port_app_config.models import (
35
19
  ResourceConfig,
36
20
  Selector,
37
21
  )
22
+ from port_ocean.core.handlers.queue import LocalQueue
23
+ from port_ocean.core.handlers.webhook.abstract_webhook_processor import (
24
+ AbstractWebhookProcessor,
25
+ )
26
+ from port_ocean.core.handlers.webhook.processor_manager import (
27
+ LiveEventsProcessorManager,
28
+ )
29
+ from port_ocean.core.handlers.webhook.webhook_event import (
30
+ EventHeaders,
31
+ EventPayload,
32
+ WebhookEvent,
33
+ WebhookEventRawResults,
34
+ )
35
+ from port_ocean.core.integrations.base import BaseIntegration
36
+ from port_ocean.core.integrations.mixins.handler import HandlerMixin
38
37
  from port_ocean.core.integrations.mixins.live_events import LiveEventsMixin
39
38
  from port_ocean.core.models import Entity
40
39
  from port_ocean.exceptions.webhook_processor import (
41
40
  RetryableError,
42
41
  WebhookEventNotSupportedError,
43
42
  )
44
- from port_ocean.core.handlers.queue import LocalQueue
43
+ from port_ocean.utils.signal import SignalHandler
45
44
 
46
45
 
47
46
  class MockProcessor(AbstractWebhookProcessor):
@@ -275,7 +274,13 @@ def mock_http_client() -> MagicMock:
275
274
  @pytest.fixture
276
275
  def mock_port_client(mock_http_client: MagicMock) -> PortClient:
277
276
  mock_port_client = PortClient(
278
- MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
277
+ MagicMock(),
278
+ MagicMock(),
279
+ MagicMock(),
280
+ MagicMock(),
281
+ MagicMock(),
282
+ MagicMock(),
283
+ MagicMock(),
279
284
  )
280
285
  mock_port_client.auth = AsyncMock()
281
286
  mock_port_client.auth.headers = AsyncMock(
@@ -18,4 +18,5 @@ def get_port_client_for_integration(
18
18
  integration_identifier=integration_identifier,
19
19
  integration_type=integration_type,
20
20
  integration_version=integration_version,
21
+ ingest_url="https://ingest.getport.io",
21
22
  )
@@ -0,0 +1,309 @@
1
+ import pytest
2
+ from unittest.mock import Mock
3
+ from http import HTTPStatus
4
+ import httpx
5
+
6
+ from port_ocean.helpers.retry import (
7
+ RetryConfig,
8
+ RetryTransport,
9
+ register_retry_config_callback,
10
+ register_on_retry_callback,
11
+ )
12
+ import port_ocean.helpers.retry as retry_module
13
+
14
+
15
+ class TestRetryConfig:
16
+ def test_default_configuration(self) -> None:
17
+ """Test RetryConfig with default parameters."""
18
+ config = RetryConfig()
19
+
20
+ assert config.max_attempts == 10
21
+ assert config.max_backoff_wait == 60.0
22
+ assert config.base_delay == 0.1
23
+ assert config.jitter_ratio == 0.1
24
+ assert config.respect_retry_after_header is True
25
+ assert config.retryable_methods == frozenset(
26
+ ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
27
+ )
28
+ assert config.retry_status_codes == frozenset(
29
+ [
30
+ HTTPStatus.TOO_MANY_REQUESTS,
31
+ HTTPStatus.BAD_GATEWAY,
32
+ HTTPStatus.SERVICE_UNAVAILABLE,
33
+ HTTPStatus.GATEWAY_TIMEOUT,
34
+ HTTPStatus.UNAUTHORIZED,
35
+ HTTPStatus.BAD_REQUEST,
36
+ ]
37
+ )
38
+ assert config.retry_after_headers == ["Retry-After"]
39
+
40
+ def test_custom_configuration(self) -> None:
41
+ """Test RetryConfig with custom parameters."""
42
+ config = RetryConfig(
43
+ max_attempts=5,
44
+ max_backoff_wait=30.0,
45
+ base_delay=2.0,
46
+ jitter_ratio=0.2,
47
+ respect_retry_after_header=False,
48
+ retryable_methods=["GET", "POST"],
49
+ retry_after_headers=["X-Custom-Retry", "Retry-After"],
50
+ additional_retry_status_codes=[418, 420],
51
+ )
52
+
53
+ assert config.max_attempts == 5
54
+ assert config.max_backoff_wait == 30.0
55
+ assert config.base_delay == 2.0
56
+ assert config.jitter_ratio == 0.2
57
+ assert config.respect_retry_after_header is False
58
+ assert config.retryable_methods == frozenset(["GET", "POST"])
59
+ # Should include defaults + additional codes
60
+ expected_codes = frozenset(
61
+ [
62
+ HTTPStatus.TOO_MANY_REQUESTS,
63
+ HTTPStatus.BAD_GATEWAY,
64
+ HTTPStatus.SERVICE_UNAVAILABLE,
65
+ HTTPStatus.GATEWAY_TIMEOUT,
66
+ HTTPStatus.UNAUTHORIZED,
67
+ HTTPStatus.BAD_REQUEST,
68
+ 418,
69
+ 420,
70
+ ]
71
+ )
72
+ assert config.retry_status_codes == expected_codes
73
+ assert config.retry_after_headers == ["X-Custom-Retry", "Retry-After"]
74
+
75
+ def test_additional_status_codes(self) -> None:
76
+ """Test that additional status codes extend defaults."""
77
+ config = RetryConfig(
78
+ additional_retry_status_codes=[418, 420],
79
+ )
80
+
81
+ # Should include defaults + additional codes
82
+ expected_codes = frozenset(
83
+ [
84
+ HTTPStatus.TOO_MANY_REQUESTS,
85
+ HTTPStatus.BAD_GATEWAY,
86
+ HTTPStatus.SERVICE_UNAVAILABLE,
87
+ HTTPStatus.GATEWAY_TIMEOUT,
88
+ HTTPStatus.UNAUTHORIZED,
89
+ HTTPStatus.BAD_REQUEST,
90
+ 418,
91
+ 420,
92
+ ]
93
+ )
94
+ assert config.retry_status_codes == expected_codes
95
+
96
+ def test_invalid_jitter_ratio(self) -> None:
97
+ """Test that invalid jitter ratio raises ValueError."""
98
+ with pytest.raises(
99
+ ValueError, match="Jitter ratio should be between 0 and 0.5"
100
+ ):
101
+ RetryConfig(jitter_ratio=0.6)
102
+
103
+ with pytest.raises(
104
+ ValueError, match="Jitter ratio should be between 0 and 0.5"
105
+ ):
106
+ RetryConfig(jitter_ratio=-0.1)
107
+
108
+ def test_empty_additional_status_codes(self) -> None:
109
+ """Test that empty additional status codes work correctly."""
110
+ config = RetryConfig(additional_retry_status_codes=[])
111
+ assert config.retry_status_codes == frozenset(
112
+ [
113
+ HTTPStatus.TOO_MANY_REQUESTS,
114
+ HTTPStatus.BAD_GATEWAY,
115
+ HTTPStatus.SERVICE_UNAVAILABLE,
116
+ HTTPStatus.GATEWAY_TIMEOUT,
117
+ HTTPStatus.UNAUTHORIZED,
118
+ HTTPStatus.BAD_REQUEST,
119
+ ]
120
+ )
121
+
122
+
123
+ class TestRetryConfigCallback:
124
+ def setup_method(self) -> None:
125
+ """Reset global callback state before each test."""
126
+ retry_module._RETRY_CONFIG_CALLBACK = None
127
+ retry_module._ON_RETRY_CALLBACK = None
128
+
129
+ def test_register_retry_config_callback(self) -> None:
130
+ """Test registering a retry config callback."""
131
+
132
+ def mock_callback() -> RetryConfig:
133
+ return RetryConfig(max_attempts=5)
134
+
135
+ register_retry_config_callback(mock_callback)
136
+
137
+ assert retry_module._RETRY_CONFIG_CALLBACK is mock_callback
138
+ config = retry_module._RETRY_CONFIG_CALLBACK()
139
+ assert config.max_attempts == 5
140
+
141
+ def test_register_on_retry_callback(self) -> None:
142
+ """Test registering an on retry callback."""
143
+
144
+ def mock_callback(request: httpx.Request) -> httpx.Request:
145
+ return request
146
+
147
+ register_on_retry_callback(mock_callback)
148
+
149
+ assert retry_module._ON_RETRY_CALLBACK is mock_callback
150
+
151
+ def test_callback_overwrite(self) -> None:
152
+ """Test that registering a new callback overwrites the previous one."""
153
+
154
+ def callback1() -> RetryConfig:
155
+ return RetryConfig(max_attempts=1)
156
+
157
+ def callback2() -> RetryConfig:
158
+ return RetryConfig(max_attempts=2)
159
+
160
+ register_retry_config_callback(callback1)
161
+ assert retry_module._RETRY_CONFIG_CALLBACK is not None
162
+ assert retry_module._RETRY_CONFIG_CALLBACK().max_attempts == 1
163
+
164
+ register_retry_config_callback(callback2)
165
+ assert retry_module._RETRY_CONFIG_CALLBACK is not None
166
+ assert retry_module._RETRY_CONFIG_CALLBACK().max_attempts == 2
167
+
168
+
169
+ class TestRetryTransport:
170
+ def setup_method(self) -> None:
171
+ """Reset global callback state before each test."""
172
+ retry_module._RETRY_CONFIG_CALLBACK = None
173
+ retry_module._ON_RETRY_CALLBACK = None
174
+
175
+ def test_retry_transport_with_direct_config(self) -> None:
176
+ """Test RetryTransport with direct retry_config parameter."""
177
+ mock_transport = Mock()
178
+ config = RetryConfig(max_attempts=5)
179
+
180
+ transport = RetryTransport(
181
+ wrapped_transport=mock_transport,
182
+ retry_config=config,
183
+ )
184
+
185
+ assert transport._retry_config is config
186
+ assert transport._retry_config.max_attempts == 5
187
+
188
+ def test_retry_transport_with_callback(self) -> None:
189
+ """Test RetryTransport using registered callback."""
190
+
191
+ def mock_callback() -> RetryConfig:
192
+ return RetryConfig(max_attempts=7)
193
+
194
+ register_retry_config_callback(mock_callback)
195
+
196
+ mock_transport = Mock()
197
+ transport = RetryTransport(wrapped_transport=mock_transport)
198
+
199
+ assert transport._retry_config.max_attempts == 7
200
+
201
+ def test_retry_transport_priority_order(self) -> None:
202
+ """Test that direct config takes priority over callback."""
203
+
204
+ def mock_callback() -> RetryConfig:
205
+ return RetryConfig(max_attempts=7)
206
+
207
+ register_retry_config_callback(mock_callback)
208
+
209
+ mock_transport = Mock()
210
+ direct_config = RetryConfig(max_attempts=5)
211
+
212
+ transport = RetryTransport(
213
+ wrapped_transport=mock_transport,
214
+ retry_config=direct_config,
215
+ )
216
+
217
+ assert transport._retry_config.max_attempts == 5 # Direct config wins
218
+
219
+ def test_retry_transport_default_config(self) -> None:
220
+ """Test RetryTransport with no config or callback."""
221
+ mock_transport = Mock()
222
+ transport = RetryTransport(wrapped_transport=mock_transport)
223
+
224
+ assert transport._retry_config.max_attempts == 10 # Default value
225
+ assert transport._retry_config.retry_after_headers == ["Retry-After"]
226
+
227
+ def test_is_retryable_method(self) -> None:
228
+ """Test _is_retryable_method functionality."""
229
+ mock_transport = Mock()
230
+ transport = RetryTransport(wrapped_transport=mock_transport)
231
+
232
+ # Test with default retryable methods
233
+ mock_request = Mock()
234
+ mock_request.method = "GET"
235
+ mock_request.extensions = {}
236
+
237
+ assert transport._is_retryable_method(mock_request) is True
238
+
239
+ mock_request.method = "POST"
240
+ assert transport._is_retryable_method(mock_request) is False
241
+
242
+ # Test with retryable extension
243
+ mock_request.extensions = {"retryable": True}
244
+ assert transport._is_retryable_method(mock_request) is True
245
+
246
+ def test_should_retry(self) -> None:
247
+ """Test _should_retry functionality."""
248
+ mock_transport = Mock()
249
+ transport = RetryTransport(wrapped_transport=mock_transport)
250
+
251
+ mock_response = Mock()
252
+ mock_response.status_code = 429
253
+ assert transport._should_retry(mock_response) is True
254
+
255
+ mock_response.status_code = 200
256
+ assert transport._should_retry(mock_response) is False
257
+
258
+ def test_parse_retry_header(self) -> None:
259
+ """Test _parse_retry_header functionality."""
260
+ mock_transport = Mock()
261
+ transport = RetryTransport(wrapped_transport=mock_transport)
262
+
263
+ # Test numeric seconds
264
+ assert transport._parse_retry_header("30") == 30.0
265
+
266
+ # Test invalid numeric
267
+ assert transport._parse_retry_header("invalid") is None
268
+
269
+ # Test ISO date (would need proper date parsing test)
270
+ # This is a basic test - actual date parsing would need more complex setup
271
+ assert (
272
+ transport._parse_retry_header("2023-12-01T12:00:00Z") is None
273
+ ) # Will fail parsing
274
+
275
+
276
+ class TestRetryConfigIntegration:
277
+ """Integration tests for retry configuration."""
278
+
279
+ def setup_method(self) -> None:
280
+ """Reset global callback state before each test."""
281
+ retry_module._RETRY_CONFIG_CALLBACK = None
282
+ retry_module._ON_RETRY_CALLBACK = None
283
+
284
+ def test_integration_style_config(self) -> None:
285
+ """Test configuration similar to GitHub integration."""
286
+
287
+ def integration_retry_config() -> RetryConfig:
288
+ return RetryConfig(
289
+ max_attempts=10,
290
+ max_backoff_wait=60.0,
291
+ base_delay=1.0,
292
+ jitter_ratio=0.1,
293
+ respect_retry_after_header=True,
294
+ retry_after_headers=["X-RateLimit-Reset", "Retry-After"],
295
+ additional_retry_status_codes=[HTTPStatus.FORBIDDEN],
296
+ )
297
+
298
+ register_retry_config_callback(integration_retry_config)
299
+
300
+ mock_transport = Mock()
301
+ transport = RetryTransport(wrapped_transport=mock_transport)
302
+
303
+ assert transport._retry_config.max_attempts == 10
304
+ assert transport._retry_config.base_delay == 1.0
305
+ assert transport._retry_config.retry_after_headers == [
306
+ "X-RateLimit-Reset",
307
+ "Retry-After",
308
+ ]
309
+ assert HTTPStatus.FORBIDDEN in transport._retry_config.retry_status_codes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.27.9
3
+ Version: 0.28.0
4
4
  Summary: Port Ocean is a CLI tool for managing your Port projects.
5
5
  Home-page: https://app.getport.io
6
6
  Keywords: ocean,port-ocean,port
@@ -56,12 +56,12 @@ port_ocean/clients/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
56
56
  port_ocean/clients/auth/auth_client.py,sha256=scxx7AYqvXoRAd8_K-Ww26oErzi5l8ZCGPc0sVKgIfA,192
57
57
  port_ocean/clients/auth/oauth_client.py,sha256=FjexH-T3v4ssRWhkHtvHXhi1EH1Vxu8vwHp3HxqfYN8,795
58
58
  port_ocean/clients/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- port_ocean/clients/port/authentication.py,sha256=r7r8Ag9WuwXy-CmgeOoj-PHbmJAQxhb2NMx8wrNzF3g,3457
60
- port_ocean/clients/port/client.py,sha256=dv0mxIOde6J-wFi1FXXZkoNPVHrZzY7RSMhNkDD9xgA,3566
59
+ port_ocean/clients/port/authentication.py,sha256=ZO1Vw1nm-NlVUPPtPS5O4GGDvRmyS3vnayWKyVyuuKc,3519
60
+ port_ocean/clients/port/client.py,sha256=hBXgU0CDseN2F-vn20JqowfVkcd6oSVmYrjn6t4TI6c,3616
61
61
  port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
62
  port_ocean/clients/port/mixins/blueprints.py,sha256=aMCG4zePsMSMjMLiGrU37h5z5_ElfMzTcTvqvOI5wXY,4683
63
63
  port_ocean/clients/port/mixins/entities.py,sha256=X2NqH00eK6TMJ3a3QEQRVQlKHzyj5l1FiPkIhonnbPg,24234
64
- port_ocean/clients/port/mixins/integrations.py,sha256=9G1vo3n9pG1t6siUmPdYtxXbfhGXKhWAWwKHr8x7tU4,11891
64
+ port_ocean/clients/port/mixins/integrations.py,sha256=5OK21zU9vBk1-SuweiQzkXP_VxofVO1cqL7Ipw-X-YM,11940
65
65
  port_ocean/clients/port/mixins/migrations.py,sha256=vdL_A_NNUogvzujyaRLIoZEu5vmKDY2BxTjoGP94YzI,1467
66
66
  port_ocean/clients/port/mixins/organization.py,sha256=A2cP5V49KnjoAXxjmnm_XGth4ftPSU0qURNfnyUyS_Y,1041
67
67
  port_ocean/clients/port/retry_transport.py,sha256=PtIZOAZ6V-ncpVysRUsPOgt8Sf01QLnTKB5YeKBxkJk,1861
@@ -70,7 +70,7 @@ port_ocean/clients/port/utils.py,sha256=osFyAjw7Y5Qf2uVSqC7_RTCQfijiL1zS74JJM0go
70
70
  port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
71
  port_ocean/config/base.py,sha256=x1gFbzujrxn7EJudRT81C6eN9WsYAb3vOHwcpcpX8Tc,6370
72
72
  port_ocean/config/dynamic.py,sha256=Lrk4JRGtR-0YKQ9DDGexX5NGFE7EJ6VoHya19YYhssM,2687
73
- port_ocean/config/settings.py,sha256=Zz_D40EXZEm0hzNdYgwdUy_s5LbJ6iMg3Zcl2n5NLUY,7686
73
+ port_ocean/config/settings.py,sha256=pknMyy3T8FP8laCxYNfIIGEjeYFd8kQA1eSPreerBAE,7768
74
74
  port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
76
76
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -140,17 +140,17 @@ port_ocean/exceptions/port_defaults.py,sha256=2a7Koy541KxMan33mU-gbauUxsumG3NT4i
140
140
  port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnqI,197
141
141
  port_ocean/exceptions/webhook_processor.py,sha256=4SnkVzVwiacH_Ip4qs1hRHa6GanhnojW_TLTdQQtm7Y,363
142
142
  port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
- port_ocean/helpers/async_client.py,sha256=LOgUlZ5Cs_WUSc8XujCVjPGvzZ_3AuFJNKPy0FKV3fA,1987
143
+ port_ocean/helpers/async_client.py,sha256=M8gKUjX8ZwRbmJ-U6KNq-p-nfGr0CwHdS0eN_pbZAJ0,2103
144
144
  port_ocean/helpers/metric/metric.py,sha256=-dw7-Eqr65AZwv0M-xPaAk98g_JS16ICBc9_UkycFbE,14543
145
145
  port_ocean/helpers/metric/utils.py,sha256=1lAgrxnZLuR_wUNDyPOPzLrm32b8cDdioob2lvnPQ1A,1619
146
- port_ocean/helpers/retry.py,sha256=VHAp6j9-Vid6aNR5sca3S0aW6b1S2oYw9vT9hi1N22U,18556
146
+ port_ocean/helpers/retry.py,sha256=QM04mzaevIUlg8HnHjeY9UT_D4k26BHx3hVkCjV_jnY,21675
147
147
  port_ocean/helpers/stream.py,sha256=_UwsThzXynxWzL8OlBT1pmb2evZBi9HaaqeAGNuTuOI,2338
148
148
  port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
149
149
  port_ocean/log/handlers.py,sha256=LJ1WAfq7wYCrBpeLPihMKmWjdSahKKXNHFMRYkbk0Co,3630
150
150
  port_ocean/log/logger_setup.py,sha256=0K3zVG0YYrYOWEV8-rCGks1o-bMRxgHXlqawu9w_tSw,2656
151
151
  port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2920
152
152
  port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
153
- port_ocean/ocean.py,sha256=83zgTEI5o2wfl8mq-iIC9DzPOZbWyCqNIy1BEjv9TOk,8824
153
+ port_ocean/ocean.py,sha256=IhsLnPqEJ2SflnBAt-byxGl9w_ULkdvd6-sxBbVQtJw,8874
154
154
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
155
155
  port_ocean/run.py,sha256=CmKz14bxfdOooNbQ5QqH1MwX-XLYVG4NgT4KbrzFaqI,2216
156
156
  port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
@@ -166,19 +166,19 @@ port_ocean/tests/clients/port/mixins/test_integrations.py,sha256=vRt_EMsLozQC1LJ
166
166
  port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=zzKYz3h8dl4Z5A2QG_924m0y9U6XTth1XYOfwNrd_24,914
167
167
  port_ocean/tests/config/test_config.py,sha256=Rk4N-ldVSOfn1p23NzdVdfqUpPrqG2cMut4Sv-sAOrw,1023
168
168
  port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
169
- port_ocean/tests/core/conftest.py,sha256=5shShx81LRuQTBwS2sDIjNJO2LSD6cUNz46SgNYzjGY,7686
169
+ port_ocean/tests/core/conftest.py,sha256=0Oql7R1iTbjPyNdUoO6M21IKknLwnCIgDRz2JQ7nf0w,7748
170
170
  port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
171
171
  port_ocean/tests/core/event_listener/test_kafka.py,sha256=PH90qk2fvdrQOSZD2QrvkGy8w_WoYb_KHGnqJ6PLHAo,2681
172
172
  port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=7XWgwUB9uVYRov4VbIz1A-7n2YLbHTTYT-4rKJxjB0A,10711
173
173
  port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=TjSj8ssIqH23VJlO5PGovbudCqDbuE2-54iNQsD9K-I,14099
174
- port_ocean/tests/core/handlers/mixins/test_live_events.py,sha256=6yUsYooBYchiZP_eYa8PN1IhiztJShBdPguoseyNMzY,12482
174
+ port_ocean/tests/core/handlers/mixins/test_live_events.py,sha256=Sbv9IZAGQoZDhf27xDjMMVYxUSie9mHltDtxLSqckmM,12548
175
175
  port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=-Jd2rUG63fZM8LuyKtCp1tt4WEqO2m5woESjs1c91sU,44428
176
176
  port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
177
177
  port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=hSh556bJM9zuELwhwnyKSfd9z06WqWXIfe-6hCl5iKI,9799
178
178
  port_ocean/tests/core/handlers/queue/test_group_queue.py,sha256=Y1BrQi5xwhk5bYDlKRWw9PenF5cqxIF2TIU_hldqji0,22801
179
179
  port_ocean/tests/core/handlers/queue/test_local_queue.py,sha256=9Ly0HzZXbs6Rbl_bstsIdInC3h2bgABU3roP9S_PnJM,2582
180
180
  port_ocean/tests/core/handlers/webhook/test_abstract_webhook_processor.py,sha256=zKwHhPAYEZoZ5Z2UETp1t--mbkS8uyvlXThB0obZTTc,3340
181
- port_ocean/tests/core/handlers/webhook/test_processor_manager.py,sha256=rqNFc-S_ZnPyDTSFTdiGcRFKbeDGfWQCH_f2UPbfcAA,52310
181
+ port_ocean/tests/core/handlers/webhook/test_processor_manager.py,sha256=16q-5NagXBv8J1TZC75h-9N4MW_8KB4GX2mmPD6gr4s,52294
182
182
  port_ocean/tests/core/handlers/webhook/test_webhook_event.py,sha256=oR4dEHLO65mp6rkfNfszZcfFoRZlB8ZWee4XetmsuIk,3181
183
183
  port_ocean/tests/core/test_utils.py,sha256=Z3kdhb5V7Svhcyy3EansdTpgHL36TL6erNtU-OPwAcI,2647
184
184
  port_ocean/tests/core/utils/test_entity_topological_sorter.py,sha256=zuq5WSPy_88PemG3mOUIHTxWMR_js1R7tOzUYlgBd68,3447
@@ -188,8 +188,9 @@ port_ocean/tests/helpers/fake_port_api.py,sha256=9rtjC6iTQMfzWK6WipkDzzG0b1IIaRm
188
188
  port_ocean/tests/helpers/fixtures.py,sha256=dinEucKDTGAD2tKwbOqqHZSHNPsDLN2HxnLqA-WXGeI,1443
189
189
  port_ocean/tests/helpers/integration.py,sha256=_RxS-RHpu11lrbhUXYPZp862HLWx8AoD7iZM6iXN8rs,1104
190
190
  port_ocean/tests/helpers/ocean_app.py,sha256=N06vcNI1klqdcNFq-PXL5vm77u-hODsOSXnj9p8d1AI,2249
191
- port_ocean/tests/helpers/port_client.py,sha256=5d6GNr8vNNSOkrz1AdOhxBUKuusr_-UPDP7AVpHasQw,599
191
+ port_ocean/tests/helpers/port_client.py,sha256=S0CXvZWUoHFWWQUOEgdkDammK9Fs3R06wx0flaMrTsg,647
192
192
  port_ocean/tests/helpers/smoke_test.py,sha256=_9aJJFRfuGJEg2D2YQJVJRmpreS6gEPHHQq8Q01x4aQ,2697
193
+ port_ocean/tests/helpers/test_retry.py,sha256=c4kS5XrQNv8r-uqPLbWVGvudTy3b_dHxhcCjm7ZWeAQ,11033
193
194
  port_ocean/tests/log/test_handlers.py,sha256=x2P2Hd6Cb3sQafIE3TRGltbbHeiFHaiEjwRn9py_03g,2165
194
195
  port_ocean/tests/test_metric.py,sha256=gDdeJcqJDQ_o3VrYrW23iZyw2NuUsyATdrygSXhcDuQ,8096
195
196
  port_ocean/tests/test_ocean.py,sha256=bsXKGTVEjwLSbR7-qSmI4GZ-EzDo0eBE3TNSMsWzYxM,1502
@@ -207,8 +208,8 @@ port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,32
207
208
  port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
208
209
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
209
210
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
210
- port_ocean-0.27.9.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
211
- port_ocean-0.27.9.dist-info/METADATA,sha256=hv3PIzzmvZ97v_UfKhQGXa0-T-GUvTF3EgCzz-IrsZM,7015
212
- port_ocean-0.27.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
213
- port_ocean-0.27.9.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
214
- port_ocean-0.27.9.dist-info/RECORD,,
211
+ port_ocean-0.28.0.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
212
+ port_ocean-0.28.0.dist-info/METADATA,sha256=uN_b7aeBqjyt-s49n-_xgjLjFsCAzgBgf0VJACg_qHA,7015
213
+ port_ocean-0.28.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
214
+ port_ocean-0.28.0.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
215
+ port_ocean-0.28.0.dist-info/RECORD,,