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
         |