port-ocean 0.18.4__py3-none-any.whl → 0.18.6__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,6 +19,7 @@ from pydispatch import dispatcher # type: ignore
19
19
  from werkzeug.local import LocalStack, LocalProxy
20
20
 
21
21
  from port_ocean.context.resource import resource
22
+ from port_ocean.exceptions.api import EmptyPortAppConfigError
22
23
  from port_ocean.exceptions.context import (
23
24
  EventContextNotFoundError,
24
25
  ResourceContextNotFoundError,
@@ -176,8 +177,14 @@ async def event_context(
176
177
  logger.info("Event started")
177
178
  try:
178
179
  yield event
179
- except:
180
+ except EmptyPortAppConfigError as e:
181
+ logger.error(
182
+ f"Skipping resync due to empty mapping: {str(e)}", exc_info=True
183
+ )
184
+ raise
185
+ except Exception as e:
180
186
  success = False
187
+ logger.error(f"Event failed with error: {str(e)}", exc_info=True)
181
188
  raise
182
189
  else:
183
190
  success = True
@@ -3,6 +3,7 @@ from typing import Any
3
3
  from loguru import logger
4
4
 
5
5
  from port_ocean.core.handlers.port_app_config.base import BasePortAppConfig
6
+ from port_ocean.exceptions.api import EmptyPortAppConfigError
6
7
 
7
8
 
8
9
  class APIPortAppConfig(BasePortAppConfig):
@@ -20,7 +21,9 @@ class APIPortAppConfig(BasePortAppConfig):
20
21
  if not config:
21
22
  logger.error(
22
23
  "The integration port app config is empty. "
24
+ f"Integration: {integration}, "
25
+ f"Config: {config}. "
23
26
  "Please make sure to configure your port app config using Port's API."
24
27
  )
25
-
28
+ raise EmptyPortAppConfigError()
26
29
  return config
@@ -220,26 +220,32 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
220
220
  )
221
221
  modified_objects = []
222
222
 
223
- try:
224
- changed_entities = await self._map_entities_compared_with_port(
225
- objects_diff[0].entity_selector_diff.passed,
226
- resource,
227
- user_agent_type
228
- )
229
- if changed_entities:
230
- logger.info("Upserting changed entities", changed_entities=len(changed_entities),
231
- total_entities=len(objects_diff[0].entity_selector_diff.passed))
232
- await self.entities_state_applier.upsert(
233
- changed_entities, user_agent_type
234
- )
235
- else:
236
- logger.info("Entities in batch didn't changed since last sync, skipping", total_entities=len(objects_diff[0].entity_selector_diff.passed))
237
- modified_objects = [ocean.port_client._reduce_entity(entity) for entity in objects_diff[0].entity_selector_diff.passed]
238
- except Exception as e:
239
- logger.warning(f"Failed to resolve batch entities with Port, falling back to upserting all entities: {str(e)}")
240
- modified_objects = await self.entities_state_applier.upsert(
241
- objects_diff[0].entity_selector_diff.passed, user_agent_type
223
+ if event.event_type == EventType.RESYNC:
224
+ try:
225
+ changed_entities = await self._map_entities_compared_with_port(
226
+ objects_diff[0].entity_selector_diff.passed,
227
+ resource,
228
+ user_agent_type
242
229
  )
230
+ if changed_entities:
231
+ logger.info("Upserting changed entities", changed_entities=len(changed_entities),
232
+ total_entities=len(objects_diff[0].entity_selector_diff.passed))
233
+ await self.entities_state_applier.upsert(
234
+ changed_entities, user_agent_type
235
+ )
236
+ else:
237
+ logger.info("Entities in batch didn't changed since last sync, skipping", total_entities=len(objects_diff[0].entity_selector_diff.passed))
238
+ modified_objects = [ocean.port_client._reduce_entity(entity) for entity in objects_diff[0].entity_selector_diff.passed]
239
+ except Exception as e:
240
+ logger.warning(f"Failed to resolve batch entities with Port, falling back to upserting all entities: {str(e)}")
241
+ modified_objects = await self.entities_state_applier.upsert(
242
+ objects_diff[0].entity_selector_diff.passed, user_agent_type
243
+ )
244
+ else:
245
+ modified_objects = await self.entities_state_applier.upsert(
246
+ objects_diff[0].entity_selector_diff.passed, user_agent_type
247
+ )
248
+
243
249
 
244
250
  return CalculationResult(
245
251
  objects_diff[0].entity_selector_diff._replace(passed=modified_objects),
@@ -13,3 +13,10 @@ class BaseAPIException(BaseOceanException, abc.ABC):
13
13
  class InternalServerException(BaseAPIException):
14
14
  def response(self) -> Response:
15
15
  return PlainTextResponse(content="Internal server error", status_code=500)
16
+
17
+
18
+ class EmptyPortAppConfigError(Exception):
19
+ """Exception raised when the Port app configuration is empty."""
20
+
21
+ def __init__(self, message: str = "Port app config is empty") -> None:
22
+ super().__init__(message)
@@ -435,66 +435,63 @@ async def test_register_raw(
435
435
  mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
436
436
  mock_resource_config: ResourceConfig,
437
437
  ) -> None:
438
- # Mock the integration settings with skip_check_diff
439
- with patch.object(ocean.config.integration, "skip_check_diff", False):
440
- kind = "service"
441
- user_agent_type = UserAgentType.exporter
442
- raw_entity = [
443
- {"id": "entity_1", "name": "entity_1", "web_url": "https://example.com"},
444
- ]
445
- expected_result = [
446
- {
447
- "identifier": "entity_1",
448
- "blueprint": "service",
449
- "name": "entity_1",
450
- "properties": {},
451
- },
452
- ]
453
438
 
454
- async with event_context(
455
- EventType.HTTP_REQUEST, trigger_type="machine"
456
- ) as event:
457
- # Use patch to mock the method instead of direct assignment
439
+ kind = "service"
440
+ user_agent_type = UserAgentType.exporter
441
+ raw_entity = [
442
+ {"id": "entity_1", "name": "entity_1", "web_url": "https://example.com"},
443
+ ]
444
+ expected_result = [
445
+ {
446
+ "identifier": "entity_1",
447
+ "blueprint": "service",
448
+ "name": "entity_1",
449
+ "properties": {"url": "https://example.com"},
450
+ },
451
+ ]
452
+
453
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
454
+ # Use patch to mock the method instead of direct assignment
455
+ with patch.object(
456
+ mock_sync_raw_mixin_with_jq_processor.port_app_config_handler,
457
+ "get_port_app_config",
458
+ return_value=PortAppConfig(
459
+ enable_merge_entity=True,
460
+ delete_dependent_entities=True,
461
+ create_missing_related_entities=False,
462
+ resources=[mock_resource_config],
463
+ ),
464
+ ):
465
+ # Ensure the event.port_app_config is set correctly
466
+ event.port_app_config = await mock_sync_raw_mixin_with_jq_processor.port_app_config_handler.get_port_app_config(
467
+ use_cache=False
468
+ )
469
+
470
+ def upsert_side_effect(
471
+ entities: list[Entity], user_agent_type: UserAgentType
472
+ ) -> list[Entity]:
473
+ # Simulate returning the passed entities
474
+ return entities
475
+
476
+ # Patch the upsert method with the side effect
458
477
  with patch.object(
459
- mock_sync_raw_mixin_with_jq_processor.port_app_config_handler,
460
- "get_port_app_config",
461
- return_value=PortAppConfig(
462
- enable_merge_entity=True,
463
- delete_dependent_entities=True,
464
- create_missing_related_entities=False,
465
- resources=[mock_resource_config],
466
- ),
478
+ mock_sync_raw_mixin_with_jq_processor.entities_state_applier,
479
+ "upsert",
480
+ side_effect=upsert_side_effect,
467
481
  ):
468
- # Ensure the event.port_app_config is set correctly
469
- event.port_app_config = await mock_sync_raw_mixin_with_jq_processor.port_app_config_handler.get_port_app_config(
470
- use_cache=False
471
- )
472
-
473
- def upsert_side_effect(
474
- entities: list[Entity], user_agent_type: UserAgentType
475
- ) -> list[Entity]:
476
- # Simulate returning the passed entities
477
- return entities
478
-
479
- # Patch the upsert method with the side effect
480
- with patch.object(
481
- mock_sync_raw_mixin_with_jq_processor.entities_state_applier,
482
- "upsert",
483
- side_effect=upsert_side_effect,
484
- ):
485
- # Call the register_raw method
486
- registered_entities = (
487
- await mock_sync_raw_mixin_with_jq_processor.register_raw(
488
- kind, raw_entity, user_agent_type
489
- )
482
+ # Call the register_raw method
483
+ registered_entities = (
484
+ await mock_sync_raw_mixin_with_jq_processor.register_raw(
485
+ kind, raw_entity, user_agent_type
490
486
  )
487
+ )
491
488
 
492
- # Assert that the registered entities match the expected results
493
- assert len(registered_entities) == len(expected_result)
494
- for entity, result in zip(registered_entities, expected_result):
495
- assert entity.identifier == result["identifier"]
496
- assert entity.blueprint == result["blueprint"]
497
- assert entity.properties == result["properties"]
489
+ # Assert that the registered entities match the expected results
490
+ assert len(registered_entities) == len(expected_result)
491
+ for entity, result in zip(registered_entities, expected_result):
492
+ assert entity.identifier == result["identifier"]
493
+ assert entity.blueprint == result["blueprint"]
494
+ assert entity.properties == result["properties"]
498
495
 
499
496
 
500
497
  @pytest.mark.asyncio
@@ -689,30 +686,26 @@ async def test_register_resource_raw_no_changes_upsert_not_called_entitiy_is_ret
689
686
  mock_sync_raw_mixin: SyncRawMixin,
690
687
  mock_port_app_config: PortAppConfig,
691
688
  ) -> None:
692
- # Mock the integration settings with skip_check_diff
693
- with patch.object(ocean.config.integration, "skip_check_diff", False):
694
- entity = Entity(identifier="1", blueprint="service")
695
- mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
696
- mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
697
- mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
698
-
699
- async with event_context(EventType.RESYNC, trigger_type="machine") as event:
700
- event.port_app_config = mock_port_app_config
701
-
702
- # Test execution
703
- result = await mock_sync_raw_mixin._register_resource_raw(
704
- mock_port_app_config.resources[
705
- 0
706
- ], # Use the first resource from the config
707
- [{"some": "data"}],
708
- UserAgentType.exporter,
709
- )
689
+ entity = Entity(identifier="1", blueprint="service")
690
+ mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
691
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
692
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
693
+
694
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
695
+ event.port_app_config = mock_port_app_config
696
+
697
+ # Test execution
698
+ result = await mock_sync_raw_mixin._register_resource_raw(
699
+ mock_port_app_config.resources[0], # Use the first resource from the config
700
+ [{"some": "data"}],
701
+ UserAgentType.exporter,
702
+ )
710
703
 
711
- # Assertions
712
- assert len(result.entity_selector_diff.passed) == 1
713
- mock_sync_raw_mixin._calculate_raw.assert_called_once()
714
- mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
715
- mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
704
+ # Assertions
705
+ assert len(result.entity_selector_diff.passed) == 1
706
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
707
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
708
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
716
709
 
717
710
 
718
711
  @pytest.mark.asyncio
@@ -720,57 +713,89 @@ async def test_register_resource_raw_with_changes_upsert_called_and_entities_are
720
713
  mock_sync_raw_mixin: SyncRawMixin,
721
714
  mock_port_app_config: PortAppConfig,
722
715
  ) -> None:
723
- # Mock the integration settings with skip_check_diff
724
- with patch.object(ocean.config.integration, "skip_check_diff", False):
725
- entity = Entity(identifier="1", blueprint="service")
726
- mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
727
- mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([entity])) # type: ignore
728
- mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
729
-
730
- async with event_context(EventType.RESYNC, trigger_type="machine") as event:
731
- event.port_app_config = mock_port_app_config
732
-
733
- # Test execution
734
- result = await mock_sync_raw_mixin._register_resource_raw(
735
- mock_port_app_config.resources[0],
736
- [{"some": "data"}],
737
- UserAgentType.exporter,
738
- )
716
+ entity = Entity(identifier="1", blueprint="service")
717
+ mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
718
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([entity])) # type: ignore
719
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
720
+
721
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
722
+ event.port_app_config = mock_port_app_config
723
+
724
+ # Test execution
725
+ result = await mock_sync_raw_mixin._register_resource_raw(
726
+ mock_port_app_config.resources[0],
727
+ [{"some": "data"}],
728
+ UserAgentType.exporter,
729
+ )
739
730
 
740
- # Assertions
741
- assert len(result.entity_selector_diff.passed) == 1
742
- mock_sync_raw_mixin._calculate_raw.assert_called_once()
743
- mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
744
- mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
731
+ # Assertions
732
+ assert len(result.entity_selector_diff.passed) == 1
733
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
734
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
735
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
745
736
 
746
737
 
747
738
  @pytest.mark.asyncio
748
739
  async def test_register_resource_raw_with_errors(
749
740
  mock_sync_raw_mixin: SyncRawMixin, mock_port_app_config: PortAppConfig
750
741
  ) -> None:
751
- # Mock the integration settings with skip_check_diff
752
- with patch.object(ocean.config.integration, "skip_check_diff", False):
753
- failed_entity = Entity(identifier="1", blueprint="service")
754
- error = Exception("Test error")
755
- mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[], failed=[failed_entity]), errors=[error], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
756
- mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
757
- mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
758
-
759
- async with event_context(EventType.RESYNC, trigger_type="machine") as event:
760
- event.port_app_config = mock_port_app_config
761
-
762
- # Test execution
763
- result = await mock_sync_raw_mixin._register_resource_raw(
764
- mock_port_app_config.resources[0],
765
- [{"some": "data"}],
766
- UserAgentType.exporter,
767
- )
742
+ failed_entity = Entity(identifier="1", blueprint="service")
743
+ error = Exception("Test error")
744
+ mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[], failed=[failed_entity]), errors=[error], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
745
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
746
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
747
+
748
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
749
+ event.port_app_config = mock_port_app_config
750
+
751
+ # Test execution
752
+ result = await mock_sync_raw_mixin._register_resource_raw(
753
+ mock_port_app_config.resources[0],
754
+ [{"some": "data"}],
755
+ UserAgentType.exporter,
756
+ )
757
+
758
+ # Assertions
759
+ assert len(result.entity_selector_diff.passed) == 0
760
+ assert len(result.entity_selector_diff.failed) == 1
761
+ assert len(result.errors) == 1
762
+ assert result.errors[0] == error
763
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
764
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
765
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
766
+
767
+
768
+ @pytest.mark.asyncio
769
+ async def test_register_resource_raw_skip_event_type_http_request_upsert_called_and_no_entitites_diff_calculation(
770
+ mock_sync_raw_mixin: SyncRawMixin,
771
+ mock_port_app_config: PortAppConfig,
772
+ mock_context: PortOceanContext,
773
+ monkeypatch: pytest.MonkeyPatch,
774
+ ) -> None:
775
+ # Mock dependencies
776
+ entity = Entity(identifier="1", blueprint="service")
777
+ calculation_result = CalculationResult(
778
+ entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]),
779
+ errors=[],
780
+ misconfigurations=[],
781
+ misonfigured_entity_keys=[],
782
+ )
783
+ mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[calculation_result]) # type: ignore
784
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock() # type: ignore
785
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
786
+
787
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
788
+ event.port_app_config = mock_port_app_config
789
+
790
+ # Test execution
791
+ result = await mock_sync_raw_mixin._register_resource_raw(
792
+ mock_port_app_config.resources[0],
793
+ [{"some": "data"}],
794
+ UserAgentType.exporter,
795
+ )
768
796
 
769
- # Assertions
770
- assert len(result.entity_selector_diff.passed) == 0
771
- assert len(result.entity_selector_diff.failed) == 1
772
- assert len(result.errors) == 1
773
- assert result.errors[0] == error
774
- mock_sync_raw_mixin._calculate_raw.assert_called_once()
775
- mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
776
- mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
797
+ # Assertions
798
+ assert len(result.entity_selector_diff.passed) == 1
799
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
800
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_not_called()
801
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
@@ -0,0 +1,67 @@
1
+ import pytest
2
+ from unittest.mock import AsyncMock
3
+
4
+ from port_ocean.core.handlers.port_app_config.api import APIPortAppConfig
5
+ from port_ocean.exceptions.api import EmptyPortAppConfigError
6
+
7
+
8
+ @pytest.fixture
9
+ def mock_context() -> AsyncMock:
10
+ context = AsyncMock()
11
+ context.port_client.get_current_integration = AsyncMock()
12
+ return context
13
+
14
+
15
+ @pytest.fixture
16
+ def api_config(mock_context: AsyncMock) -> APIPortAppConfig:
17
+ return APIPortAppConfig(mock_context)
18
+
19
+
20
+ async def test_get_port_app_config_valid_config_returns_config(
21
+ api_config: APIPortAppConfig, mock_context: AsyncMock
22
+ ) -> None:
23
+ # Arrange
24
+ expected_config = {"key": "value"}
25
+ mock_context.port_client.get_current_integration.return_value = {
26
+ "config": expected_config
27
+ }
28
+
29
+ # Act
30
+ result = await api_config._get_port_app_config()
31
+
32
+ # Assert
33
+ assert result == expected_config
34
+ mock_context.port_client.get_current_integration.assert_called_once()
35
+
36
+
37
+ async def test_get_port_app_config_empty_config_raises_value_error(
38
+ api_config: APIPortAppConfig, mock_context: AsyncMock
39
+ ) -> None:
40
+ # Arrange
41
+ mock_context.port_client.get_current_integration.return_value = {"config": {}}
42
+
43
+ # Act & Assert
44
+ with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"):
45
+ await api_config._get_port_app_config()
46
+
47
+
48
+ async def test_get_port_app_config_missing_config_key_raises_key_error(
49
+ api_config: APIPortAppConfig, mock_context: AsyncMock
50
+ ) -> None:
51
+ # Arrange
52
+ mock_context.port_client.get_current_integration.return_value = {}
53
+
54
+ # Act & Assert
55
+ with pytest.raises(KeyError):
56
+ await api_config._get_port_app_config()
57
+
58
+
59
+ async def test_get_port_app_config_empty_integration_raises_key_error(
60
+ api_config: APIPortAppConfig, mock_context: AsyncMock
61
+ ) -> None:
62
+ # Arrange
63
+ mock_context.port_client.get_current_integration.return_value = {}
64
+
65
+ # Act & Assert
66
+ with pytest.raises(KeyError):
67
+ await api_config._get_port_app_config()
@@ -0,0 +1,197 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+ from pydantic import ValidationError
4
+ from typing import Any, Dict
5
+
6
+ from port_ocean.context.ocean import PortOceanContext
7
+ from port_ocean.core.handlers.port_app_config.base import BasePortAppConfig
8
+ from port_ocean.core.handlers.port_app_config.models import PortAppConfig
9
+ from port_ocean.context.event import EventType, event_context
10
+ from port_ocean.exceptions.api import EmptyPortAppConfigError
11
+
12
+
13
+ class TestPortAppConfig(BasePortAppConfig):
14
+ mock_get_port_app_config: Any
15
+
16
+ async def _get_port_app_config(self) -> Dict[str, Any]:
17
+ return self.mock_get_port_app_config()
18
+
19
+
20
+ @pytest.fixture
21
+ def mock_context() -> PortOceanContext:
22
+ context = MagicMock(spec=PortOceanContext)
23
+ context.config.port.port_app_config_cache_ttl = 300 # 5 minutes
24
+ return context
25
+
26
+
27
+ @pytest.fixture
28
+ def port_app_config_handler(mock_context: PortOceanContext) -> TestPortAppConfig:
29
+ handler = TestPortAppConfig(mock_context)
30
+ handler.mock_get_port_app_config = MagicMock()
31
+ return handler
32
+
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_get_port_app_config_success(
36
+ port_app_config_handler: TestPortAppConfig,
37
+ ) -> None:
38
+ # Arrange
39
+ valid_config = {
40
+ "resources": [
41
+ {
42
+ "kind": "repository",
43
+ "selector": {"query": "true"},
44
+ "port": {
45
+ "entity": {
46
+ "mappings": {
47
+ "identifier": ".name",
48
+ "title": ".name",
49
+ "blueprint": '"service"',
50
+ "properties": {
51
+ "description": ".description",
52
+ "url": ".html_url",
53
+ "defaultBranch": ".default_branch",
54
+ },
55
+ }
56
+ }
57
+ },
58
+ }
59
+ ]
60
+ }
61
+ port_app_config_handler.mock_get_port_app_config.return_value = valid_config
62
+
63
+ # Act
64
+ async with event_context(EventType.RESYNC, trigger_type="machine"):
65
+ result = await port_app_config_handler.get_port_app_config()
66
+
67
+ # Assert
68
+ assert isinstance(result, PortAppConfig)
69
+ assert result.resources[0].port.entity.mappings.title == ".name"
70
+ assert result.resources[0].port.entity.mappings.identifier == ".name"
71
+ assert result.resources[0].port.entity.mappings.blueprint == '"service"'
72
+ assert (
73
+ result.resources[0].port.entity.mappings.properties["description"]
74
+ == ".description"
75
+ )
76
+ assert result.resources[0].port.entity.mappings.properties["url"] == ".html_url"
77
+ assert (
78
+ result.resources[0].port.entity.mappings.properties["defaultBranch"]
79
+ == ".default_branch"
80
+ )
81
+ port_app_config_handler.mock_get_port_app_config.assert_called_once()
82
+
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_get_port_app_config_uses_cache(
86
+ port_app_config_handler: TestPortAppConfig,
87
+ ) -> None:
88
+ # Arrange
89
+ valid_config = {
90
+ "resources": [
91
+ {
92
+ "kind": "repository",
93
+ "selector": {"query": "true"},
94
+ "port": {
95
+ "entity": {
96
+ "mappings": {
97
+ "identifier": ".name",
98
+ "title": ".name",
99
+ "blueprint": '"service"',
100
+ "properties": {
101
+ "description": ".description",
102
+ "url": ".html_url",
103
+ "defaultBranch": ".default_branch",
104
+ },
105
+ }
106
+ }
107
+ },
108
+ }
109
+ ]
110
+ }
111
+ port_app_config_handler.mock_get_port_app_config.return_value = valid_config
112
+
113
+ # Act
114
+ async with event_context(EventType.RESYNC, trigger_type="machine"):
115
+ result1 = await port_app_config_handler.get_port_app_config()
116
+ result2 = await port_app_config_handler.get_port_app_config()
117
+
118
+ # Assert
119
+ assert result1 == result2
120
+ port_app_config_handler.mock_get_port_app_config.assert_called_once() # Called only once due to caching
121
+
122
+
123
+ @pytest.mark.asyncio
124
+ async def test_get_port_app_config_bypass_cache(
125
+ port_app_config_handler: TestPortAppConfig,
126
+ ) -> None:
127
+ # Arrange
128
+ valid_config = {
129
+ "resources": [
130
+ {
131
+ "kind": "repository",
132
+ "selector": {"query": "true"},
133
+ "port": {
134
+ "entity": {
135
+ "mappings": {
136
+ "identifier": ".name",
137
+ "title": ".name",
138
+ "blueprint": '"service"',
139
+ "properties": {
140
+ "description": ".description",
141
+ "url": ".html_url",
142
+ "defaultBranch": ".default_branch",
143
+ },
144
+ }
145
+ }
146
+ },
147
+ }
148
+ ]
149
+ }
150
+ port_app_config_handler.mock_get_port_app_config.return_value = valid_config
151
+
152
+ # Act
153
+ async with event_context(EventType.RESYNC, trigger_type="machine"):
154
+ result1 = await port_app_config_handler.get_port_app_config()
155
+ result2 = await port_app_config_handler.get_port_app_config(use_cache=False)
156
+
157
+ # Assert
158
+ assert result1 == result2
159
+ assert (
160
+ port_app_config_handler.mock_get_port_app_config.call_count == 2
161
+ ) # Called twice due to cache bypass
162
+
163
+
164
+ @pytest.mark.asyncio
165
+ async def test_get_port_app_config_validation_error(
166
+ port_app_config_handler: TestPortAppConfig, monkeypatch: pytest.MonkeyPatch
167
+ ) -> None:
168
+ # Arrange
169
+ invalid_config = {"invalid_field": "invalid_value"}
170
+ port_app_config_handler.mock_get_port_app_config.return_value = invalid_config
171
+
172
+ def mock_parse_obj(*args: Any, **kwargs: Any) -> None:
173
+ raise ValidationError(errors=[], model=PortAppConfig)
174
+
175
+ monkeypatch.setattr(
176
+ port_app_config_handler.CONFIG_CLASS, "parse_obj", mock_parse_obj
177
+ )
178
+
179
+ # Act & Assert
180
+ with pytest.raises(ValidationError):
181
+ async with event_context(EventType.RESYNC, trigger_type="machine"):
182
+ await port_app_config_handler.get_port_app_config()
183
+
184
+
185
+ @pytest.mark.asyncio
186
+ async def test_get_port_app_config_fetch_error(
187
+ port_app_config_handler: TestPortAppConfig,
188
+ ) -> None:
189
+ # Arrange
190
+ port_app_config_handler.mock_get_port_app_config.side_effect = (
191
+ EmptyPortAppConfigError("Port app config is empty")
192
+ )
193
+
194
+ # Act & Assert
195
+ async with event_context(EventType.RESYNC, trigger_type="machine"):
196
+ with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"):
197
+ await port_app_config_handler.get_port_app_config()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.18.4
3
+ Version: 0.18.6
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
@@ -63,7 +63,7 @@ port_ocean/config/settings.py,sha256=q5KgDIr8snIxejoKyWSyf21R_AYEEXiEd3-ry9qf3ss
63
63
  port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
64
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
65
65
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- port_ocean/context/event.py,sha256=tf254jMqBW1GBmYDhfXMCkOqHA7C_chaYp1OY3Dfnsg,5869
66
+ port_ocean/context/event.py,sha256=QK2ben4fJtxdorq_yRroATttP0DRc4wLtlUJ1as5D58,6208
67
67
  port_ocean/context/ocean.py,sha256=0kgIi0zAsGarF52Qehu4bOxnAFEb0yayzG7xZioMHJc,4993
68
68
  port_ocean/context/resource.py,sha256=yDj63URzQelj8zJPh4BAzTtPhpKr9Gw9DRn7I_0mJ1s,1692
69
69
  port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -91,7 +91,7 @@ port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybe
91
91
  port_ocean/core/handlers/entity_processor/base.py,sha256=udR0w5TstTOS5xOfTjAZIEdldn4xr6Oyb3DylatYX3Q,1869
92
92
  port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=C6zJbS3miKyDeXiEV-0t5vJvkEznOeXRZFFOnwJcNdA,11714
93
93
  port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iEgEeXtn2pRMrT4Dze5r1Ixbk,134
94
- port_ocean/core/handlers/port_app_config/api.py,sha256=6VbKPwFzsWG0IYsVD81hxSmfqtHUFqrfUuj1DBX5g4w,853
94
+ port_ocean/core/handlers/port_app_config/api.py,sha256=r_Th66NEw38IpRdnXZcRvI8ACfvxW_A6V62WLwjWXlQ,1044
95
95
  port_ocean/core/handlers/port_app_config/base.py,sha256=4Nxt2g8voEIHJ4Y1Km5NJcaG2iSbCklw5P8-Kus7Y9k,3007
96
96
  port_ocean/core/handlers/port_app_config/models.py,sha256=YvYtf_44KD_rN4xK-3xHtdpRZ1M8Qo-m9K4LDtH7FYU,2344
97
97
  port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
@@ -102,7 +102,7 @@ port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4
102
102
  port_ocean/core/integrations/mixins/events.py,sha256=0jKRsBw6lU8Mqs7MaQK4n-t_H6Z4NEkXZ5VWzqTrKEc,2396
103
103
  port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
104
104
  port_ocean/core/integrations/mixins/sync.py,sha256=B9fEs8faaYLLikH9GBjE_E61vo0bQDjIGQsQ1SRXOlA,3931
105
- port_ocean/core/integrations/mixins/sync_raw.py,sha256=XLxH9_OOudLW4Q5lrUWWpcXIM3KdEDnYwEEP661Rfao,24465
105
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=xDGz4pOfpd6G_pMQveEHQCDdOTk-Uun_aP0G_0IxsDA,24784
106
106
  port_ocean/core/integrations/mixins/utils.py,sha256=oN4Okz6xlaefpid1_Pud8HPSw9BwwjRohyNsknq-Myg,2309
107
107
  port_ocean/core/models.py,sha256=FvTp-BlpbvLbMbngE0wsiimsCfmIhUR1PvsE__Z--1I,2206
108
108
  port_ocean/core/ocean_types.py,sha256=j_-or1VxDy22whLLxwxgzIsE4wAhFLH19Xff9l4oJA8,1124
@@ -110,7 +110,7 @@ port_ocean/core/utils/entity_topological_sorter.py,sha256=MDUjM6OuDy4Xj68o-7InNN
110
110
  port_ocean/core/utils/utils.py,sha256=HmumOeH27N0NX1_OP3t4oGKt074ht9XyXhvfZ5I05s4,6474
111
111
  port_ocean/debug_cli.py,sha256=gHrv-Ey3cImKOcGZpjoHlo4pa_zfmyOl6TUM4o9VtcA,96
112
112
  port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
- port_ocean/exceptions/api.py,sha256=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
113
+ port_ocean/exceptions/api.py,sha256=1JcA-H12lhSgolMEA6dM4JMbDrh9sYDcE7oydPSTuK8,649
114
114
  port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
115
115
  port_ocean/exceptions/clients.py,sha256=LKLLs-Zy3caNG85rwxfOw2rMr8qqVV6SHUq4fRCZ99U,180
116
116
  port_ocean/exceptions/context.py,sha256=mA8HII6Rl4QxKUz98ppy1zX3kaziaen21h1ZWuU3ADc,372
@@ -135,7 +135,9 @@ port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=-8iHM33Oe
135
135
  port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
136
136
  port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
137
137
  port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=FnEnaDjuoAbKvKyv6xJ46n3j0ZcaT70Sg2zc7oy7HAA,13596
138
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=SSf1ZRZVcIta9zfx2-SpYFc_-MoHPDJSa1MkbIx3icI,31172
138
+ port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=gxQ4e9hQuMS8-o5UbiUSt1I1uaK0DCO3yCFDVigpZvo,31740
139
+ port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
140
+ port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=s3D98JP3YV9V6T5PCDPE2852gkqGiDmo03UyexESX_I,6872
139
141
  port_ocean/tests/core/test_utils.py,sha256=Z3kdhb5V7Svhcyy3EansdTpgHL36TL6erNtU-OPwAcI,2647
140
142
  port_ocean/tests/core/utils/test_entity_topological_sorter.py,sha256=zuq5WSPy_88PemG3mOUIHTxWMR_js1R7tOzUYlgBd68,3447
141
143
  port_ocean/tests/core/utils/test_resolve_entities_diff.py,sha256=4kTey1c0dWKbLXjJ-9m2GXrHyAWZeLQ2asdtYRRUdRs,16573
@@ -160,8 +162,8 @@ port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,32
160
162
  port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
161
163
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
162
164
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
163
- port_ocean-0.18.4.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
164
- port_ocean-0.18.4.dist-info/METADATA,sha256=qe_3HL2F4NgoCId8n4gmLPqVUGKaOTU11XC05vMPO8I,6669
165
- port_ocean-0.18.4.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
166
- port_ocean-0.18.4.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
167
- port_ocean-0.18.4.dist-info/RECORD,,
165
+ port_ocean-0.18.6.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
166
+ port_ocean-0.18.6.dist-info/METADATA,sha256=71enLFwwGK7wlTo-ETXIVzGAtGQ5VHEFfs0y75kAtP0,6669
167
+ port_ocean-0.18.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
168
+ port_ocean-0.18.6.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
169
+ port_ocean-0.18.6.dist-info/RECORD,,