port-ocean 0.17.8__py3-none-any.whl → 0.18.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of port-ocean might be problematic. Click here for more details.
- port_ocean/clients/port/mixins/entities.py +21 -6
- port_ocean/core/integrations/mixins/sync_raw.py +136 -24
- port_ocean/core/models.py +4 -0
- port_ocean/core/utils/utils.py +80 -4
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +309 -2
- port_ocean/tests/core/utils/test_resolve_entities_diff.py +559 -0
- {port_ocean-0.17.8.dist-info → port_ocean-0.18.1.dist-info}/METADATA +1 -1
- {port_ocean-0.17.8.dist-info → port_ocean-0.18.1.dist-info}/RECORD +11 -10
- {port_ocean-0.17.8.dist-info → port_ocean-0.18.1.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.17.8.dist-info → port_ocean-0.18.1.dist-info}/WHEEL +0 -0
- {port_ocean-0.17.8.dist-info → port_ocean-0.18.1.dist-info}/entry_points.txt +0 -0
|
@@ -29,6 +29,8 @@ from port_ocean.core.models import Entity
|
|
|
29
29
|
from port_ocean.context.event import EventContext, event_context, EventType
|
|
30
30
|
from port_ocean.clients.port.types import UserAgentType
|
|
31
31
|
from port_ocean.context.ocean import ocean
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import List, Optional
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
@pytest.fixture
|
|
@@ -141,6 +143,26 @@ def mock_entity_processor(mock_context: PortOceanContext) -> JQEntityProcessor:
|
|
|
141
143
|
return JQEntityProcessor(mock_context)
|
|
142
144
|
|
|
143
145
|
|
|
146
|
+
@pytest.fixture
|
|
147
|
+
def mock_resource_config() -> ResourceConfig:
|
|
148
|
+
resource = ResourceConfig(
|
|
149
|
+
kind="service",
|
|
150
|
+
selector=Selector(query="true"),
|
|
151
|
+
port=PortResourceConfig(
|
|
152
|
+
entity=MappingsConfig(
|
|
153
|
+
mappings=EntityMapping(
|
|
154
|
+
identifier=".id",
|
|
155
|
+
title=".name",
|
|
156
|
+
blueprint='"service"',
|
|
157
|
+
properties={"url": ".web_url"},
|
|
158
|
+
relations={},
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
return resource
|
|
164
|
+
|
|
165
|
+
|
|
144
166
|
@pytest.fixture
|
|
145
167
|
def mock_entities_state_applier(
|
|
146
168
|
mock_context: PortOceanContext,
|
|
@@ -410,7 +432,10 @@ async def test_sync_raw_mixin_dependency(
|
|
|
410
432
|
|
|
411
433
|
@pytest.mark.asyncio
|
|
412
434
|
async def test_register_raw(
|
|
413
|
-
mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
|
|
435
|
+
mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
|
|
436
|
+
mock_ocean: Ocean,
|
|
437
|
+
mock_context: PortOceanContext,
|
|
438
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
414
439
|
) -> None:
|
|
415
440
|
kind = "service"
|
|
416
441
|
user_agent_type = UserAgentType.exporter
|
|
@@ -426,6 +451,9 @@ async def test_register_raw(
|
|
|
426
451
|
},
|
|
427
452
|
]
|
|
428
453
|
|
|
454
|
+
# Set is_saas to False
|
|
455
|
+
monkeypatch.setattr(mock_context.app, "is_saas", lambda: False)
|
|
456
|
+
|
|
429
457
|
async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
|
|
430
458
|
# Use patch to mock the method instead of direct assignment
|
|
431
459
|
with patch.object(
|
|
@@ -488,7 +516,10 @@ async def test_register_raw(
|
|
|
488
516
|
|
|
489
517
|
@pytest.mark.asyncio
|
|
490
518
|
async def test_unregister_raw(
|
|
491
|
-
mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
|
|
519
|
+
mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
|
|
520
|
+
mock_ocean: Ocean,
|
|
521
|
+
mock_context: PortOceanContext,
|
|
522
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
492
523
|
) -> None:
|
|
493
524
|
kind = "service"
|
|
494
525
|
user_agent_type = UserAgentType.exporter
|
|
@@ -504,6 +535,9 @@ async def test_unregister_raw(
|
|
|
504
535
|
},
|
|
505
536
|
]
|
|
506
537
|
|
|
538
|
+
# Set is_saas to False
|
|
539
|
+
monkeypatch.setattr(mock_context.app, "is_saas", lambda: False)
|
|
540
|
+
|
|
507
541
|
async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
|
|
508
542
|
# Use patch to mock the method instead of direct assignment
|
|
509
543
|
with patch.object(
|
|
@@ -550,3 +584,276 @@ async def test_unregister_raw(
|
|
|
550
584
|
assert entity.identifier == result["identifier"]
|
|
551
585
|
assert entity.blueprint == result["blueprint"]
|
|
552
586
|
assert entity.properties == result["properties"]
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@pytest.mark.asyncio
|
|
590
|
+
async def test_map_entities_compared_with_port_no_port_entities_all_entities_are_mapped(
|
|
591
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
592
|
+
mock_ocean: Ocean,
|
|
593
|
+
mock_resource_config: ResourceConfig,
|
|
594
|
+
) -> None:
|
|
595
|
+
# Setup test data
|
|
596
|
+
entities = [
|
|
597
|
+
create_entity("entity_1", "service", {}, False),
|
|
598
|
+
create_entity("entity_2", "service", {}, False),
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
# Mock port client to return empty list
|
|
602
|
+
mock_ocean.port_client.search_entities.return_value = [] # type: ignore
|
|
603
|
+
|
|
604
|
+
# Execute test
|
|
605
|
+
changed_entities = await mock_sync_raw_mixin._map_entities_compared_with_port(
|
|
606
|
+
entities, mock_resource_config, UserAgentType.exporter
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Verify results
|
|
610
|
+
assert len(changed_entities) == 2
|
|
611
|
+
assert "entity_1" in [e.identifier for e in changed_entities]
|
|
612
|
+
assert "entity_2" in [e.identifier for e in changed_entities]
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@pytest.mark.asyncio
|
|
616
|
+
async def test_map_entities_compared_with_port_with_existing_entities_only_changed_third_party_entities_are_mapped(
|
|
617
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
618
|
+
mock_ocean: Ocean,
|
|
619
|
+
mock_resource_config: ResourceConfig,
|
|
620
|
+
) -> None:
|
|
621
|
+
# Setup test data
|
|
622
|
+
third_party_entities = [
|
|
623
|
+
create_entity(
|
|
624
|
+
"entity_1", "service", {"service": "entity_2"}, False
|
|
625
|
+
), # Should be in changed (modified)
|
|
626
|
+
create_entity("entity_2", "service", {}, False), # Should be in changed (new)
|
|
627
|
+
]
|
|
628
|
+
port_entities = [
|
|
629
|
+
create_entity("entity_1", "service", {}, False), # Existing but different props
|
|
630
|
+
create_entity("entity_3", "service", {}, False), # Should be in irrelevant
|
|
631
|
+
]
|
|
632
|
+
|
|
633
|
+
# Mock port client to return our port entities
|
|
634
|
+
mock_ocean.port_client.search_entities.return_value = port_entities # type: ignore
|
|
635
|
+
|
|
636
|
+
changed_entities = await mock_sync_raw_mixin._map_entities_compared_with_port(
|
|
637
|
+
third_party_entities, mock_resource_config, UserAgentType.exporter
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
# Verify results
|
|
641
|
+
assert len(changed_entities) == 2
|
|
642
|
+
assert "entity_1" in [e.identifier for e in changed_entities]
|
|
643
|
+
assert "entity_2" in [e.identifier for e in changed_entities]
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@pytest.mark.asyncio
|
|
647
|
+
async def test_map_entities_compared_with_port_with_multiple_batches_all_batches_are_being_proccessed_to_map(
|
|
648
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
649
|
+
mock_ocean: Ocean,
|
|
650
|
+
mock_resource_config: ResourceConfig,
|
|
651
|
+
) -> None:
|
|
652
|
+
# Setup test data with 75 entities (should create 2 batches)
|
|
653
|
+
third_party_entities = [
|
|
654
|
+
create_entity(
|
|
655
|
+
f"entity_{i}", "service", {"service_relation": f"service_{i}"}, False
|
|
656
|
+
)
|
|
657
|
+
for i in range(75)
|
|
658
|
+
]
|
|
659
|
+
port_entities_batch1 = [
|
|
660
|
+
create_entity(f"entity_{i}", "service", {}, False) for i in range(50)
|
|
661
|
+
]
|
|
662
|
+
port_entities_batch2 = [
|
|
663
|
+
create_entity(f"entity_{i}", "service", {}, False) for i in range(50, 75)
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
# Mock port client to return our port entities in batches
|
|
667
|
+
with patch.object(
|
|
668
|
+
mock_ocean.port_client,
|
|
669
|
+
"search_entities",
|
|
670
|
+
new_callable=AsyncMock,
|
|
671
|
+
side_effect=[port_entities_batch1, port_entities_batch2],
|
|
672
|
+
) as mock_search_entities:
|
|
673
|
+
# Mock resolve_entities_diff to return all entities
|
|
674
|
+
with patch(
|
|
675
|
+
"port_ocean.core.integrations.mixins.sync_raw.resolve_entities_diff",
|
|
676
|
+
return_value=third_party_entities,
|
|
677
|
+
) as mock_resolve_entities_diff:
|
|
678
|
+
# Execute test
|
|
679
|
+
changed_entities = (
|
|
680
|
+
await mock_sync_raw_mixin._map_entities_compared_with_port(
|
|
681
|
+
third_party_entities, mock_resource_config, UserAgentType.exporter
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Verify results
|
|
686
|
+
assert len(changed_entities) == 75
|
|
687
|
+
assert [e.identifier for e in changed_entities] == [
|
|
688
|
+
f"entity_{i}" for i in range(75)
|
|
689
|
+
]
|
|
690
|
+
assert (
|
|
691
|
+
mock_search_entities.call_count == 2
|
|
692
|
+
) # Verify two batch calls were made
|
|
693
|
+
assert (
|
|
694
|
+
mock_resolve_entities_diff.call_count == 1
|
|
695
|
+
) # Verify final diff was calculated once
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@dataclass
|
|
699
|
+
class EntitySelectorDiff:
|
|
700
|
+
passed: List[Entity]
|
|
701
|
+
failed: List[Entity]
|
|
702
|
+
|
|
703
|
+
def _replace(self, **kwargs: Any) -> "EntitySelectorDiff":
|
|
704
|
+
return EntitySelectorDiff(
|
|
705
|
+
**{
|
|
706
|
+
"passed": kwargs.get("passed", self.passed),
|
|
707
|
+
"failed": kwargs.get("failed", self.failed),
|
|
708
|
+
}
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
@dataclass
|
|
713
|
+
class CalculationResult:
|
|
714
|
+
entity_selector_diff: EntitySelectorDiff
|
|
715
|
+
errors: List[Any]
|
|
716
|
+
misconfigurations: List[Any]
|
|
717
|
+
misonfigured_entity_keys: Optional[List[Any]] = None
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@pytest.mark.asyncio
|
|
721
|
+
async def test_register_resource_raw_saas_no_changes_upsert_not_called_entitiy_is_returned(
|
|
722
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
723
|
+
mock_port_app_config: PortAppConfig,
|
|
724
|
+
mock_context: PortOceanContext,
|
|
725
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
726
|
+
) -> None:
|
|
727
|
+
# Mock ocean.app.is_saas()
|
|
728
|
+
monkeypatch.setattr(mock_context.app, "is_saas", lambda: True)
|
|
729
|
+
|
|
730
|
+
# Mock dependencies
|
|
731
|
+
entity = Entity(identifier="1", blueprint="service")
|
|
732
|
+
mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
|
|
733
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
|
|
734
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
|
|
735
|
+
|
|
736
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
737
|
+
event.port_app_config = mock_port_app_config
|
|
738
|
+
|
|
739
|
+
# Test execution
|
|
740
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
|
741
|
+
mock_port_app_config.resources[0], # Use the first resource from the config
|
|
742
|
+
[{"some": "data"}],
|
|
743
|
+
UserAgentType.exporter,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Assertions
|
|
747
|
+
assert len(result.entity_selector_diff.passed) == 1
|
|
748
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
|
749
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
|
|
750
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
@pytest.mark.asyncio
|
|
754
|
+
async def test_register_resource_raw_saas_with_changes_upsert_called_and_entities_are_mapped(
|
|
755
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
756
|
+
mock_port_app_config: PortAppConfig,
|
|
757
|
+
mock_context: PortOceanContext,
|
|
758
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
759
|
+
) -> None:
|
|
760
|
+
# Mock ocean.app.is_saas()
|
|
761
|
+
monkeypatch.setattr(mock_context.app, "is_saas", lambda: True)
|
|
762
|
+
|
|
763
|
+
# Mock dependencies
|
|
764
|
+
entity = Entity(identifier="1", blueprint="service")
|
|
765
|
+
mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
|
|
766
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([entity])) # type: ignore
|
|
767
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
|
|
768
|
+
|
|
769
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
770
|
+
event.port_app_config = mock_port_app_config
|
|
771
|
+
|
|
772
|
+
# Test execution
|
|
773
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
|
774
|
+
mock_port_app_config.resources[0],
|
|
775
|
+
[{"some": "data"}],
|
|
776
|
+
UserAgentType.exporter,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# Assertions
|
|
780
|
+
assert len(result.entity_selector_diff.passed) == 1
|
|
781
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
|
782
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
|
|
783
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
@pytest.mark.asyncio
|
|
787
|
+
async def test_register_resource_raw_non_saas_upsert_called_and_no_entitites_diff_calculation(
|
|
788
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
789
|
+
mock_port_app_config: PortAppConfig,
|
|
790
|
+
mock_context: PortOceanContext,
|
|
791
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
792
|
+
) -> None:
|
|
793
|
+
# Mock ocean.app.is_saas()
|
|
794
|
+
monkeypatch.setattr(mock_context.app, "is_saas", lambda: False)
|
|
795
|
+
|
|
796
|
+
# Mock dependencies
|
|
797
|
+
entity = Entity(identifier="1", blueprint="service")
|
|
798
|
+
calculation_result = CalculationResult(
|
|
799
|
+
entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]),
|
|
800
|
+
errors=[],
|
|
801
|
+
misconfigurations=[],
|
|
802
|
+
misonfigured_entity_keys=[],
|
|
803
|
+
)
|
|
804
|
+
mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[calculation_result]) # type: ignore
|
|
805
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock() # type: ignore
|
|
806
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
|
|
807
|
+
|
|
808
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
809
|
+
event.port_app_config = mock_port_app_config
|
|
810
|
+
|
|
811
|
+
# Test execution
|
|
812
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
|
813
|
+
mock_port_app_config.resources[0],
|
|
814
|
+
[{"some": "data"}],
|
|
815
|
+
UserAgentType.exporter,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Assertions
|
|
819
|
+
assert len(result.entity_selector_diff.passed) == 1
|
|
820
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
|
821
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_not_called()
|
|
822
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
@pytest.mark.asyncio
|
|
826
|
+
async def test_register_resource_raw_saas_with_errors(
|
|
827
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
828
|
+
mock_port_app_config: PortAppConfig,
|
|
829
|
+
mock_context: PortOceanContext,
|
|
830
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
831
|
+
) -> None:
|
|
832
|
+
# Mock ocean.app.is_saas()
|
|
833
|
+
monkeypatch.setattr(mock_context.app, "is_saas", lambda: True)
|
|
834
|
+
|
|
835
|
+
# Mock dependencies
|
|
836
|
+
failed_entity = Entity(identifier="1", blueprint="service")
|
|
837
|
+
error = Exception("Test error")
|
|
838
|
+
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
|
|
839
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
|
|
840
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
|
|
841
|
+
|
|
842
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
843
|
+
event.port_app_config = mock_port_app_config
|
|
844
|
+
|
|
845
|
+
# Test execution
|
|
846
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
|
847
|
+
mock_port_app_config.resources[0],
|
|
848
|
+
[{"some": "data"}],
|
|
849
|
+
UserAgentType.exporter,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# Assertions
|
|
853
|
+
assert len(result.entity_selector_diff.passed) == 0
|
|
854
|
+
assert len(result.entity_selector_diff.failed) == 1
|
|
855
|
+
assert len(result.errors) == 1
|
|
856
|
+
assert result.errors[0] == error
|
|
857
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
|
858
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
|
|
859
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
|