port-ocean 0.18.9__py3-none-any.whl → 0.19.2__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.
- port_ocean/clients/auth/__init__.py +0 -0
- port_ocean/clients/auth/auth_client.py +10 -0
- port_ocean/clients/auth/oauth_client.py +22 -0
- port_ocean/clients/port/mixins/integrations.py +45 -4
- port_ocean/config/settings.py +4 -3
- port_ocean/core/defaults/initialize.py +16 -4
- port_ocean/helpers/retry.py +15 -1
- port_ocean/ocean.py +19 -7
- port_ocean/tests/clients/__init__.py +0 -0
- port_ocean/tests/clients/oauth/__init__.py +0 -0
- port_ocean/tests/clients/oauth/test_oauth_client.py +96 -0
- port_ocean/tests/test_ocean.py +49 -0
- port_ocean/utils/async_http.py +1 -1
- port_ocean/utils/repeat.py +0 -2
- {port_ocean-0.18.9.dist-info → port_ocean-0.19.2.dist-info}/METADATA +1 -1
- {port_ocean-0.18.9.dist-info → port_ocean-0.19.2.dist-info}/RECORD +19 -12
- {port_ocean-0.18.9.dist-info → port_ocean-0.19.2.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.18.9.dist-info → port_ocean-0.19.2.dist-info}/WHEEL +0 -0
- {port_ocean-0.18.9.dist-info → port_ocean-0.19.2.dist-info}/entry_points.txt +0 -0
File without changes
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from port_ocean.clients.auth.auth_client import AuthClient
|
2
|
+
from port_ocean.context.ocean import ocean
|
3
|
+
from port_ocean.helpers.retry import register_on_retry_callback
|
4
|
+
|
5
|
+
|
6
|
+
class OAuthClient(AuthClient):
|
7
|
+
def __init__(self) -> None:
|
8
|
+
"""
|
9
|
+
A client that can refresh a request using an access token.
|
10
|
+
"""
|
11
|
+
if self.is_oauth_enabled():
|
12
|
+
register_on_retry_callback(self.refresh_request_auth_creds)
|
13
|
+
|
14
|
+
def is_oauth_enabled(self) -> bool:
|
15
|
+
return ocean.app.load_external_oauth_access_token() is not None
|
16
|
+
|
17
|
+
@property
|
18
|
+
def external_access_token(self) -> str:
|
19
|
+
access_token = ocean.app.load_external_oauth_access_token()
|
20
|
+
if access_token is None:
|
21
|
+
raise ValueError("No external access token found")
|
22
|
+
return access_token
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import asyncio
|
2
|
-
from typing import Any, Dict, TYPE_CHECKING, Optional, TypedDict
|
2
|
+
from typing import Any, Dict, List, TYPE_CHECKING, Optional, TypedDict
|
3
3
|
from urllib.parse import quote_plus
|
4
4
|
|
5
5
|
import httpx
|
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
|
14
14
|
|
15
15
|
|
16
16
|
INTEGRATION_POLLING_INTERVAL_INITIAL_SECONDS = 3
|
17
|
-
INTEGRATION_POLLING_INTERVAL_BACKOFF_FACTOR = 1.
|
17
|
+
INTEGRATION_POLLING_INTERVAL_BACKOFF_FACTOR = 1.55
|
18
18
|
INTEGRATION_POLLING_RETRY_LIMIT = 30
|
19
19
|
CREATE_RESOURCES_PARAM_NAME = "integration_modes"
|
20
20
|
CREATE_RESOURCES_PARAM_VALUE = ["create_resources"]
|
@@ -38,6 +38,27 @@ class IntegrationClientMixin:
|
|
38
38
|
self.client = client
|
39
39
|
self._log_attributes: LogAttributes | None = None
|
40
40
|
|
41
|
+
async def is_integration_provision_enabled(
|
42
|
+
self, integration_type: str, should_raise: bool = True, should_log: bool = True
|
43
|
+
) -> bool:
|
44
|
+
enabled_integrations = await self.get_provision_enabled_integrations(
|
45
|
+
should_raise, should_log
|
46
|
+
)
|
47
|
+
return integration_type in enabled_integrations
|
48
|
+
|
49
|
+
async def get_provision_enabled_integrations(
|
50
|
+
self, should_raise: bool = True, should_log: bool = True
|
51
|
+
) -> List[str]:
|
52
|
+
logger.info("Fetching provision enabled integrations")
|
53
|
+
response = await self.client.get(
|
54
|
+
f"{self.auth.api_url}/integration/provision-enabled",
|
55
|
+
headers=await self.auth.headers(),
|
56
|
+
)
|
57
|
+
|
58
|
+
handle_status_code(response, should_raise, should_log)
|
59
|
+
|
60
|
+
return response.json().get("integrations", [])
|
61
|
+
|
41
62
|
async def _get_current_integration(self) -> httpx.Response:
|
42
63
|
logger.info(f"Fetching integration with id: {self.integration_identifier}")
|
43
64
|
response = await self.client.get(
|
@@ -51,7 +72,27 @@ class IntegrationClientMixin:
|
|
51
72
|
) -> dict[str, Any]:
|
52
73
|
response = await self._get_current_integration()
|
53
74
|
handle_status_code(response, should_raise, should_log)
|
54
|
-
|
75
|
+
integration = response.json().get("integration", {})
|
76
|
+
if integration.get("config", None) or not integration:
|
77
|
+
return integration
|
78
|
+
is_provision_enabled_for_integration = integration.get(
|
79
|
+
"installationAppType", None
|
80
|
+
) and (
|
81
|
+
await self.is_integration_provision_enabled(
|
82
|
+
integration.get("installationAppType", ""),
|
83
|
+
should_raise,
|
84
|
+
should_log,
|
85
|
+
)
|
86
|
+
)
|
87
|
+
|
88
|
+
if is_provision_enabled_for_integration:
|
89
|
+
logger.info(
|
90
|
+
"integration type is enabled, polling until provisioning is complete"
|
91
|
+
)
|
92
|
+
integration = (
|
93
|
+
await self._poll_integration_until_default_provisioning_is_complete()
|
94
|
+
)
|
95
|
+
return integration
|
55
96
|
|
56
97
|
async def get_log_attributes(self) -> LogAttributes:
|
57
98
|
if self._log_attributes is None:
|
@@ -72,7 +113,7 @@ class IntegrationClientMixin:
|
|
72
113
|
)
|
73
114
|
response = await self._get_current_integration()
|
74
115
|
integration_json = response.json()
|
75
|
-
if integration_json.get("integration", {}).get("config", {}):
|
116
|
+
if integration_json.get("integration", {}).get("config", {}) != {}:
|
76
117
|
return integration_json
|
77
118
|
|
78
119
|
logger.info(
|
port_ocean/config/settings.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
from typing import Any, Literal, Type, cast
|
2
2
|
|
3
|
-
from pydantic import
|
3
|
+
from pydantic import AnyHttpUrl, Extra, parse_obj_as, parse_raw_as
|
4
4
|
from pydantic.class_validators import root_validator, validator
|
5
|
-
from pydantic.env_settings import
|
5
|
+
from pydantic.env_settings import BaseSettings, EnvSettingsSource, InitSettingsSource
|
6
6
|
from pydantic.fields import Field
|
7
7
|
from pydantic.main import BaseModel
|
8
8
|
|
9
|
-
from port_ocean.config.base import
|
9
|
+
from port_ocean.config.base import BaseOceanModel, BaseOceanSettings
|
10
10
|
from port_ocean.core.event_listener import EventListenerSettingsType
|
11
11
|
from port_ocean.core.models import CreatePortResourcesOrigin, Runtime
|
12
12
|
from port_ocean.utils.misc import get_integration_name, get_spec_file
|
@@ -71,6 +71,7 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
|
|
71
71
|
# Determines if Port should generate resources such as blueprints and pages instead of ocean
|
72
72
|
create_port_resources_origin: CreatePortResourcesOrigin | None = None
|
73
73
|
send_raw_data_examples: bool = True
|
74
|
+
oauth_access_token_file_path: str | None = None
|
74
75
|
port: PortSettings
|
75
76
|
event_listener: EventListenerSettingsType = Field(
|
76
77
|
default=cast(EventListenerSettingsType, {"type": "POLLING"})
|
@@ -75,7 +75,7 @@ async def _initialize_required_integration_settings(
|
|
75
75
|
create_port_resources_origin_in_port=integration_config.create_port_resources_origin
|
76
76
|
== CreatePortResourcesOrigin.Port,
|
77
77
|
)
|
78
|
-
elif not integration.get("config"):
|
78
|
+
elif not integration.get("config", None):
|
79
79
|
logger.info(
|
80
80
|
"Encountered that the integration's mapping is empty, Initializing to default mapping"
|
81
81
|
)
|
@@ -213,11 +213,20 @@ async def _initialize_defaults(
|
|
213
213
|
config_class, integration_config.resources_path
|
214
214
|
)
|
215
215
|
|
216
|
+
is_integration_provision_enabled = (
|
217
|
+
await port_client.is_integration_provision_enabled(
|
218
|
+
integration_config.integration.type
|
219
|
+
)
|
220
|
+
)
|
221
|
+
|
216
222
|
if (
|
217
223
|
not integration_config.create_port_resources_origin
|
218
|
-
and
|
224
|
+
and is_integration_provision_enabled
|
219
225
|
):
|
220
|
-
|
226
|
+
# Need to set default since spec is missing
|
227
|
+
logger.info(
|
228
|
+
f"Setting resources origin to be Port (integration {integration_config.integration.type} is supported)"
|
229
|
+
)
|
221
230
|
integration_config.create_port_resources_origin = CreatePortResourcesOrigin.Port
|
222
231
|
|
223
232
|
if (
|
@@ -228,7 +237,10 @@ async def _initialize_defaults(
|
|
228
237
|
"Resources origin is set to be Port, verifying integration is supported"
|
229
238
|
)
|
230
239
|
org_feature_flags = await port_client.get_organization_feature_flags()
|
231
|
-
if
|
240
|
+
if (
|
241
|
+
not is_integration_provision_enabled
|
242
|
+
or ORG_USE_PROVISIONED_DEFAULTS_FEATURE_FLAG not in org_feature_flags
|
243
|
+
):
|
232
244
|
logger.info(
|
233
245
|
"Port origin for Integration is not supported, changing resources origin to use Ocean"
|
234
246
|
)
|
port_ocean/helpers/retry.py
CHANGED
@@ -9,6 +9,15 @@ from typing import Any, Callable, Coroutine, Iterable, Mapping, Union
|
|
9
9
|
import httpx
|
10
10
|
from dateutil.parser import isoparse
|
11
11
|
|
12
|
+
_ON_RETRY_CALLBACK: Callable[[httpx.Request], httpx.Request] | None = None
|
13
|
+
|
14
|
+
|
15
|
+
def register_on_retry_callback(
|
16
|
+
_on_retry_callback: Callable[[httpx.Request], httpx.Request]
|
17
|
+
) -> None:
|
18
|
+
global _ON_RETRY_CALLBACK
|
19
|
+
_ON_RETRY_CALLBACK = _on_retry_callback
|
20
|
+
|
12
21
|
|
13
22
|
# Adapted from https://github.com/encode/httpx/issues/108#issuecomment-1434439481
|
14
23
|
class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
@@ -43,7 +52,6 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
|
43
52
|
_retry_status_codes (frozenset): The HTTP status codes that can be retried.
|
44
53
|
_jitter_ratio (float): The amount of jitter to add to the backoff time.
|
45
54
|
_max_backoff_wait (float): The maximum time to wait between retries in seconds.
|
46
|
-
|
47
55
|
"""
|
48
56
|
|
49
57
|
RETRYABLE_METHODS = frozenset(["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"])
|
@@ -53,6 +61,8 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
|
53
61
|
HTTPStatus.BAD_GATEWAY,
|
54
62
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
55
63
|
HTTPStatus.GATEWAY_TIMEOUT,
|
64
|
+
HTTPStatus.UNAUTHORIZED,
|
65
|
+
HTTPStatus.BAD_REQUEST,
|
56
66
|
]
|
57
67
|
)
|
58
68
|
MAX_BACKOFF_WAIT_IN_SECONDS = 60
|
@@ -316,6 +326,8 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
|
316
326
|
if remaining_attempts < 1:
|
317
327
|
self._log_error(request, error)
|
318
328
|
raise
|
329
|
+
if _ON_RETRY_CALLBACK:
|
330
|
+
request = _ON_RETRY_CALLBACK(request)
|
319
331
|
attempts_made += 1
|
320
332
|
remaining_attempts -= 1
|
321
333
|
|
@@ -357,5 +369,7 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
|
357
369
|
if remaining_attempts < 1:
|
358
370
|
self._log_error(request, error)
|
359
371
|
raise
|
372
|
+
if _ON_RETRY_CALLBACK:
|
373
|
+
request = _ON_RETRY_CALLBACK(request)
|
360
374
|
attempts_made += 1
|
361
375
|
remaining_attempts -= 1
|
port_ocean/ocean.py
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
import asyncio
|
2
2
|
import sys
|
3
|
-
import threading
|
4
3
|
from contextlib import asynccontextmanager
|
5
|
-
|
4
|
+
import threading
|
5
|
+
from typing import Any, AsyncIterator, Callable, Dict, Type
|
6
6
|
|
7
|
-
from fastapi import
|
7
|
+
from fastapi import APIRouter, FastAPI
|
8
8
|
from loguru import logger
|
9
9
|
from pydantic import BaseModel
|
10
|
-
from starlette.types import
|
10
|
+
from starlette.types import Receive, Scope, Send
|
11
11
|
|
12
|
-
from port_ocean.core.handlers.resync_state_updater import ResyncStateUpdater
|
13
12
|
from port_ocean.clients.port.client import PortClient
|
14
13
|
from port_ocean.config.settings import (
|
15
14
|
IntegrationConfiguration,
|
16
15
|
)
|
17
16
|
from port_ocean.context.ocean import (
|
18
17
|
PortOceanContext,
|
19
|
-
ocean,
|
20
18
|
initialize_port_ocean_context,
|
19
|
+
ocean,
|
21
20
|
)
|
21
|
+
from port_ocean.core.handlers.resync_state_updater import ResyncStateUpdater
|
22
22
|
from port_ocean.core.integrations.base import BaseIntegration
|
23
23
|
from port_ocean.log.sensetive import sensitive_log_filter
|
24
24
|
from port_ocean.middlewares import request_handler
|
25
|
+
from port_ocean.utils.misc import IntegrationStateStatus
|
25
26
|
from port_ocean.utils.repeat import repeat_every
|
26
27
|
from port_ocean.utils.signal import signal_handler
|
27
28
|
from port_ocean.version import __integration_version__
|
28
|
-
from port_ocean.utils.misc import IntegrationStateStatus
|
29
29
|
from port_ocean.core.handlers.webhook.processor_manager import WebhookProcessorManager
|
30
30
|
|
31
31
|
|
@@ -118,6 +118,18 @@ class Ocean:
|
|
118
118
|
)
|
119
119
|
await repeated_function()
|
120
120
|
|
121
|
+
def load_external_oauth_access_token(self) -> str | None:
|
122
|
+
if self.config.oauth_access_token_file_path is not None:
|
123
|
+
try:
|
124
|
+
with open(self.config.oauth_access_token_file_path, "r") as f:
|
125
|
+
return f.read()
|
126
|
+
except Exception:
|
127
|
+
logger.exception(
|
128
|
+
"Failed to load external oauth access token from file",
|
129
|
+
file_path=self.config.oauth_access_token_file_path,
|
130
|
+
)
|
131
|
+
return None
|
132
|
+
|
121
133
|
def initialize_app(self) -> None:
|
122
134
|
self.fast_api_app.include_router(self.integration_router, prefix="/integration")
|
123
135
|
|
File without changes
|
File without changes
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import pytest
|
2
|
+
import httpx
|
3
|
+
from unittest.mock import MagicMock, patch
|
4
|
+
from port_ocean.ocean import Ocean
|
5
|
+
|
6
|
+
from port_ocean.clients.auth.oauth_client import OAuthClient
|
7
|
+
from port_ocean.config.settings import IntegrationConfiguration
|
8
|
+
|
9
|
+
|
10
|
+
@pytest.fixture
|
11
|
+
def mock_ocean() -> Ocean:
|
12
|
+
with patch("port_ocean.ocean.Ocean.__init__", return_value=None):
|
13
|
+
ocean_mock = Ocean()
|
14
|
+
ocean_mock.config = MagicMock(spec=IntegrationConfiguration)
|
15
|
+
return ocean_mock
|
16
|
+
|
17
|
+
|
18
|
+
class MockOAuthClient(OAuthClient):
|
19
|
+
def __init__(self, is_oauth_enabled_value: bool = True):
|
20
|
+
self._is_oauth_enabled = is_oauth_enabled_value
|
21
|
+
self._access_token = "mock_access_token"
|
22
|
+
super().__init__()
|
23
|
+
|
24
|
+
def is_oauth_enabled(self) -> bool:
|
25
|
+
return self._is_oauth_enabled
|
26
|
+
|
27
|
+
def refresh_request_auth_creds(self, request: httpx.Request) -> httpx.Request:
|
28
|
+
headers = dict(request.headers)
|
29
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
30
|
+
return httpx.Request(
|
31
|
+
method=request.method,
|
32
|
+
url=request.url,
|
33
|
+
headers=headers,
|
34
|
+
content=request.content,
|
35
|
+
)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def access_token(self) -> str:
|
39
|
+
return self._access_token
|
40
|
+
|
41
|
+
|
42
|
+
@pytest.fixture
|
43
|
+
def mock_oauth_client() -> MockOAuthClient:
|
44
|
+
return MockOAuthClient()
|
45
|
+
|
46
|
+
|
47
|
+
@pytest.fixture
|
48
|
+
def disabled_oauth_client() -> MockOAuthClient:
|
49
|
+
return MockOAuthClient(is_oauth_enabled_value=False)
|
50
|
+
|
51
|
+
|
52
|
+
def test_oauth_client_initialization(mock_oauth_client: MockOAuthClient) -> None:
|
53
|
+
assert isinstance(mock_oauth_client, OAuthClient)
|
54
|
+
assert mock_oauth_client.is_oauth_enabled() is True
|
55
|
+
|
56
|
+
|
57
|
+
def test_oauth_client_disabled_initialization(
|
58
|
+
disabled_oauth_client: MockOAuthClient,
|
59
|
+
) -> None:
|
60
|
+
assert isinstance(disabled_oauth_client, OAuthClient)
|
61
|
+
assert disabled_oauth_client.is_oauth_enabled() is False
|
62
|
+
|
63
|
+
|
64
|
+
def test_refresh_request_auth_creds(mock_oauth_client: MockOAuthClient) -> None:
|
65
|
+
# Create request with some content and existing headers
|
66
|
+
original_headers = {"Accept": "application/json", "X-Custom": "value"}
|
67
|
+
original_content = b'{"key": "value"}'
|
68
|
+
original_request = httpx.Request(
|
69
|
+
"GET",
|
70
|
+
"https://api.example.com",
|
71
|
+
headers=original_headers,
|
72
|
+
content=original_content,
|
73
|
+
)
|
74
|
+
|
75
|
+
refreshed_request = mock_oauth_client.refresh_request_auth_creds(original_request)
|
76
|
+
|
77
|
+
# Verify all attributes are identical except headers
|
78
|
+
assert refreshed_request.method == original_request.method
|
79
|
+
assert refreshed_request.url == original_request.url
|
80
|
+
assert refreshed_request.content == original_request.content
|
81
|
+
|
82
|
+
# Verify headers: should contain all original headers plus the new Authorization
|
83
|
+
for key, value in original_headers.items():
|
84
|
+
assert refreshed_request.headers[key] == value
|
85
|
+
assert refreshed_request.headers["Authorization"] == "Bearer mock_access_token"
|
86
|
+
# New headers should be:
|
87
|
+
# {'host': 'api.example.com',
|
88
|
+
# 'accept': 'application/json',
|
89
|
+
# 'x-custom': 'value',
|
90
|
+
# 'content-length': '16',
|
91
|
+
# 'authorization': '[secure]'}
|
92
|
+
assert len(refreshed_request.headers) == 5
|
93
|
+
|
94
|
+
|
95
|
+
def test_access_token_property(mock_oauth_client: MockOAuthClient) -> None:
|
96
|
+
assert mock_oauth_client.access_token == "mock_access_token"
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import MagicMock, mock_open, patch
|
3
|
+
from port_ocean.ocean import Ocean
|
4
|
+
from port_ocean.config.settings import IntegrationConfiguration
|
5
|
+
|
6
|
+
|
7
|
+
@pytest.fixture
|
8
|
+
def mock_ocean() -> Ocean:
|
9
|
+
with patch("port_ocean.ocean.Ocean.__init__", return_value=None):
|
10
|
+
ocean_mock = Ocean()
|
11
|
+
ocean_mock.config = MagicMock(spec=IntegrationConfiguration)
|
12
|
+
return ocean_mock
|
13
|
+
|
14
|
+
|
15
|
+
def test_load_external_oauth_access_token_no_file(mock_ocean: Ocean) -> None:
|
16
|
+
# Setup
|
17
|
+
mock_ocean.config.oauth_access_token_file_path = None
|
18
|
+
|
19
|
+
# Execute
|
20
|
+
result = mock_ocean.load_external_oauth_access_token()
|
21
|
+
|
22
|
+
# Assert
|
23
|
+
assert result is None
|
24
|
+
|
25
|
+
|
26
|
+
def test_load_external_oauth_access_token_with_file(mock_ocean: Ocean) -> None:
|
27
|
+
# Setup
|
28
|
+
mock_ocean.config.oauth_access_token_file_path = "/path/to/token.txt"
|
29
|
+
mock_file_content = "test_access_token"
|
30
|
+
|
31
|
+
with patch("builtins.open", mock_open(read_data=mock_file_content)):
|
32
|
+
# Execute
|
33
|
+
result = mock_ocean.load_external_oauth_access_token()
|
34
|
+
|
35
|
+
# Assert
|
36
|
+
assert result == "test_access_token"
|
37
|
+
|
38
|
+
|
39
|
+
def test_load_external_oauth_access_token_with_empty_file(mock_ocean: Ocean) -> None:
|
40
|
+
# Setup
|
41
|
+
mock_ocean.config.oauth_access_token_file_path = "/path/to/token.txt"
|
42
|
+
mock_file_content = ""
|
43
|
+
|
44
|
+
with patch("builtins.open", mock_open(read_data=mock_file_content)):
|
45
|
+
# Execute
|
46
|
+
result = mock_ocean.load_external_oauth_access_token()
|
47
|
+
|
48
|
+
# Assert
|
49
|
+
assert result == ""
|
port_ocean/utils/async_http.py
CHANGED
@@ -18,7 +18,7 @@ def _get_http_client_context() -> httpx.AsyncClient:
|
|
18
18
|
|
19
19
|
|
20
20
|
"""
|
21
|
-
Utilize this client for all outbound integration requests to the third-party application. It functions as a wrapper
|
21
|
+
Utilize this client for all outbound integration requests to the third-party application. It functions as a wrapper
|
22
22
|
around the httpx.AsyncClient, incorporating retry logic at the transport layer for handling retries on 5xx errors and
|
23
23
|
connection errors.
|
24
24
|
|
port_ocean/utils/repeat.py
CHANGED
@@ -22,10 +22,8 @@ def repeat_every(
|
|
22
22
|
) -> NoArgsNoReturnDecorator:
|
23
23
|
"""
|
24
24
|
This function returns a decorator that modifies a function so it is periodically re-executed after its first call.
|
25
|
-
|
26
25
|
The function it decorates should accept no arguments and return nothing. If necessary, this can be accomplished
|
27
26
|
by using `functools.partial` or otherwise wrapping the target function prior to decoration.
|
28
|
-
|
29
27
|
Parameters
|
30
28
|
----------
|
31
29
|
seconds: float
|
@@ -44,13 +44,16 @@ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py,
|
|
44
44
|
port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py,sha256=Ew5LA_G1k6DC5a2ygU2FoyjZQa0fRmPy73N0bio0d14,46
|
45
45
|
port_ocean/cli/utils.py,sha256=IUK2UbWqjci-lrcDdynZXqVP5B5TcjF0w5CpEVUks-k,54
|
46
46
|
port_ocean/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
|
+
port_ocean/clients/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
48
|
+
port_ocean/clients/auth/auth_client.py,sha256=scxx7AYqvXoRAd8_K-Ww26oErzi5l8ZCGPc0sVKgIfA,192
|
49
|
+
port_ocean/clients/auth/oauth_client.py,sha256=RGMigXP8XOQECvEccXjF-EYPBXaxFDpA2aniN_vM3d0,794
|
47
50
|
port_ocean/clients/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
48
51
|
port_ocean/clients/port/authentication.py,sha256=6-uDMWsJ0xLe1-9IoYXHWmwtufj8rJR4BCRXJlSkCSQ,3447
|
49
52
|
port_ocean/clients/port/client.py,sha256=OaNeN3U7Hw0tK4jYE6ESJEPKbTf9nGp2jcJVq00gnf8,3546
|
50
53
|
port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
54
|
port_ocean/clients/port/mixins/blueprints.py,sha256=POBl4uDocrgJBw4rvCAzwRcD4jk-uBL6pDAuKMTajdg,4633
|
52
55
|
port_ocean/clients/port/mixins/entities.py,sha256=PJzVZTBW_OheFRGPRCZ6yPbVGEAKsMO9CNDNJUI1l48,10770
|
53
|
-
port_ocean/clients/port/mixins/integrations.py,sha256=
|
56
|
+
port_ocean/clients/port/mixins/integrations.py,sha256=0ht8nfjsRBu_dbnlMZxSH0jqKv0PF8U8EkllRXgnrWA,9122
|
54
57
|
port_ocean/clients/port/mixins/migrations.py,sha256=A6896oJF6WbFL2WroyTkMzr12yhVyWqGoq9dtLNSKBY,1457
|
55
58
|
port_ocean/clients/port/mixins/organization.py,sha256=fCo_ZS8UlQXsyIx-odTuWkbnfcYmVnQfIsSyJuPOPjM,1031
|
56
59
|
port_ocean/clients/port/retry_transport.py,sha256=PtIZOAZ6V-ncpVysRUsPOgt8Sf01QLnTKB5YeKBxkJk,1861
|
@@ -59,7 +62,7 @@ port_ocean/clients/port/utils.py,sha256=SjhgmJXAqH2JqXfGy8GoGwzUYiJvUhWDrJyxQcen
|
|
59
62
|
port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
63
|
port_ocean/config/base.py,sha256=x1gFbzujrxn7EJudRT81C6eN9WsYAb3vOHwcpcpX8Tc,6370
|
61
64
|
port_ocean/config/dynamic.py,sha256=qOFkRoJsn_BW7581omi_AoMxoHqasf_foxDQ_G11_SI,2030
|
62
|
-
port_ocean/config/settings.py,sha256=
|
65
|
+
port_ocean/config/settings.py,sha256=C1hQJsvHz-411-CtT0DvMGkImcO-E_zgmbzpI3TUO64,5279
|
63
66
|
port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
67
|
port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
|
65
68
|
port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -70,7 +73,7 @@ port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
73
|
port_ocean/core/defaults/__init__.py,sha256=8qCZg8n06WAdMu9s_FiRtDYLGPGHbOuS60vapeUoAks,142
|
71
74
|
port_ocean/core/defaults/clean.py,sha256=_rL-NCl6Q_x3lUxDW5ACOM27IYilTCWl6ISUfRleuL0,2891
|
72
75
|
port_ocean/core/defaults/common.py,sha256=zJsj7jvlqIMLGXhdASUlbKS8GIAf-FDKKB0O7jB6nx0,4166
|
73
|
-
port_ocean/core/defaults/initialize.py,sha256=
|
76
|
+
port_ocean/core/defaults/initialize.py,sha256=3Ezn63YwxVnByegRPqNXt057Tx9D0IxqrZQHgkrbKWA,10760
|
74
77
|
port_ocean/core/event_listener/__init__.py,sha256=T3E52MKs79fNEW381p7zU9F2vOMvIiiTYWlqRUqnsg0,1135
|
75
78
|
port_ocean/core/event_listener/base.py,sha256=VdIdp7RLOSxH3ICyV-wCD3NiJoUzsh2KkJ0a9B29GeI,2847
|
76
79
|
port_ocean/core/event_listener/factory.py,sha256=M4Qi05pI840sjDIbdjUEgYe9Gp5ckoCkX-KgLBxUpZg,4096
|
@@ -127,17 +130,20 @@ port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnq
|
|
127
130
|
port_ocean/exceptions/webhook_processor.py,sha256=yQYazg53Y-ohb7HfViwq1opH_ZUuUdhHSRxcUNveFpI,114
|
128
131
|
port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
129
132
|
port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861vE3K40,1783
|
130
|
-
port_ocean/helpers/retry.py,sha256=
|
133
|
+
port_ocean/helpers/retry.py,sha256=1SxeRPkaH3K1BDvcdZbze2ila7SyOMYD8DQ3CwrruYk,16135
|
131
134
|
port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
132
135
|
port_ocean/log/handlers.py,sha256=ncVjgqrZRh6BhyRrA6DQG86Wsbxph1yWYuEC0cWfe-Q,3631
|
133
136
|
port_ocean/log/logger_setup.py,sha256=CoEDowe5OwNOL_5clU6Z4faktfh0VWaOTS0VLmyhHjw,2404
|
134
137
|
port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2920
|
135
138
|
port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
|
136
|
-
port_ocean/ocean.py,sha256=
|
139
|
+
port_ocean/ocean.py,sha256=jFNVykBy01b4A7er16bzQoiMYZqfRgcs8TR0Snd3FKY,6046
|
137
140
|
port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
138
141
|
port_ocean/run.py,sha256=COoRSmLG4hbsjIW5DzhV0NYVegI9xHd1POv6sg4U1No,2217
|
139
142
|
port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
|
140
143
|
port_ocean/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
144
|
+
port_ocean/tests/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
145
|
+
port_ocean/tests/clients/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
146
|
+
port_ocean/tests/clients/oauth/test_oauth_client.py,sha256=2XVMQUalDpiD539Z7_dk5BK_ngXQzsTmb2lNBsfEm9c,3266
|
141
147
|
port_ocean/tests/clients/port/mixins/test_entities.py,sha256=A9myrnkLhKSQrnOLv1Zz2wiOVSxW65Q9RIUIRbn_V7w,1586
|
142
148
|
port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=-8iHM33Oe8PuyEfj3O_6Yob8POp4fSmB0hnIT0Gv-8Y,868
|
143
149
|
port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
|
@@ -162,21 +168,22 @@ port_ocean/tests/helpers/ocean_app.py,sha256=N06vcNI1klqdcNFq-PXL5vm77u-hODsOSXn
|
|
162
168
|
port_ocean/tests/helpers/port_client.py,sha256=5d6GNr8vNNSOkrz1AdOhxBUKuusr_-UPDP7AVpHasQw,599
|
163
169
|
port_ocean/tests/helpers/smoke_test.py,sha256=_9aJJFRfuGJEg2D2YQJVJRmpreS6gEPHHQq8Q01x4aQ,2697
|
164
170
|
port_ocean/tests/log/test_handlers.py,sha256=uxgYCEQLP9U5qf-zUN9SgWFogMbYdnBeOVzXZ7E_yFw,2119
|
171
|
+
port_ocean/tests/test_ocean.py,sha256=bsXKGTVEjwLSbR7-qSmI4GZ-EzDo0eBE3TNSMsWzYxM,1502
|
165
172
|
port_ocean/tests/test_smoke.py,sha256=uix2uIg_yOm8BHDgHw2hTFPy1fiIyxBGW3ENU_KoFlo,2557
|
166
173
|
port_ocean/tests/utils/test_async_iterators.py,sha256=3PLk1emEXekb8LcC5GgVh3OicaX15i5WyaJT_eFnu_4,1336
|
167
174
|
port_ocean/tests/utils/test_cache.py,sha256=GzoS8xGCBDbBcPwSDbdimsMMkRvJATrBC7UmFhdW3fw,4906
|
168
175
|
port_ocean/utils/__init__.py,sha256=KMGnCPXZJbNwtgxtyMycapkDz8tpSyw23MSYT3iVeHs,91
|
169
|
-
port_ocean/utils/async_http.py,sha256=
|
176
|
+
port_ocean/utils/async_http.py,sha256=pdXUs5VxvWcX0i6wJXb_LjN0OrunEkCGdoM_TNXcSO0,1225
|
170
177
|
port_ocean/utils/async_iterators.py,sha256=CPXskYWkhkZtAG-ducEwM8537t3z5usPEqXR9vcivzw,3715
|
171
178
|
port_ocean/utils/cache.py,sha256=RgfN4SjjHrEkbqUChyboeD1mrXomolUUjsJtvbkmr3U,3353
|
172
179
|
port_ocean/utils/misc.py,sha256=0q2cJ5psqxn_5u_56pT7vOVQ3shDM02iC1lzyWQ_zl0,2098
|
173
180
|
port_ocean/utils/queue_utils.py,sha256=KWWl8YVnG-glcfIHhM6nefY-2sou_C6DVP1VynQwzB4,2762
|
174
|
-
port_ocean/utils/repeat.py,sha256=
|
181
|
+
port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,3229
|
175
182
|
port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
|
176
183
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
177
184
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
178
|
-
port_ocean-0.
|
179
|
-
port_ocean-0.
|
180
|
-
port_ocean-0.
|
181
|
-
port_ocean-0.
|
182
|
-
port_ocean-0.
|
185
|
+
port_ocean-0.19.2.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
186
|
+
port_ocean-0.19.2.dist-info/METADATA,sha256=-jaMpn-0NtgOvDlvVbIwTPuRwbRMh2kDmbGYRLzdEqQ,6669
|
187
|
+
port_ocean-0.19.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
188
|
+
port_ocean-0.19.2.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
189
|
+
port_ocean-0.19.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|