port-ocean 0.15.2__py3-none-any.whl → 0.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of port-ocean might be problematic. Click here for more details.

@@ -77,18 +77,15 @@ def new(path: str, is_private_integration: bool) -> None:
77
77
  )
78
78
  console.print("Here are your next steps:\n", style="bold")
79
79
  console.print(
80
- "⚓️ Install necessary packages: Run [bold][blue]make install[/blue][/bold] to install all required packages for your project.\n"
81
- f"▶️ [bold][blue]cd {path}/{name} && make install && . .venv/bin/activate[/blue][/bold]\n"
80
+ f"⚓️ Install necessary packages: Run [bold][blue]cd {path}/{name} && make install && . .venv/bin/activate[/blue][/bold] to install all required packages for your project."
82
81
  )
83
82
  console.print(
84
- f"⚓️ Copy example env file: Run [bold][blue]cp {path}/{name}.env.example {path}/{name}/.env [/blue][/bold] and set your port credentials in the created file.\n"
83
+ "⚓️ Copy example env file: Run [bold][blue]cp .env.example .env [/blue][/bold] and update your integration's configuration in the .env file."
85
84
  )
86
85
  console.print(
87
- "⚓️ Set sail with [blue]Ocean[/blue]: Run [bold][blue]ocean sail[/blue] <path_to_integration>[/bold] to run the project using Ocean.\n"
88
- f"▶️ [bold][blue]ocean sail {path}/{name}[/blue][/bold] \n"
86
+ "⚓️ Set sail with [blue]Ocean[/blue]: Run [bold][blue]ocean sail[/blue][/bold] to run the project using Ocean."
89
87
  )
90
88
  if not final_private_integration:
91
89
  console.print(
92
- "⚓️ Smooth sailing with [blue]Make[/blue]: Alternatively, you can run [bold][blue]make run[/blue][/bold] to launch your project using Make. \n"
93
- f"▶️ [bold][blue]make run {path}/{name}[/blue][/bold]"
90
+ f"⚓️ Smooth sailing with [blue]Make[/blue]: Alternatively, you can run [bold][blue]make run {path}/{name}[/blue][/bold] to launch your project using Make."
94
91
  )
@@ -6,6 +6,9 @@
6
6
  "email": "Your address email <you@example.com>",
7
7
  "release_date": "{% now 'local' %}",
8
8
  "is_private_integration": true,
9
+ "port_client_id": "you can find it using: https://docs.getport.io/build-your-software-catalog/custom-integration/api/#find-your-port-credentials",
10
+ "port_client_secret": "you can find it using: https://docs.getport.io/build-your-software-catalog/custom-integration/api/#find-your-port-credentials",
11
+ "is_us_region": false,
9
12
  "_extensions": [
10
13
  "jinja2_time.TimeExtension",
11
14
  "extensions.VersionExtension"
@@ -1,2 +1,6 @@
1
- OCEAN__PORT__CLIENT_ID="<port-client-id>"
2
- OCEAN__PORT__CLIENT_SECRET="<port-client-secret>"
1
+ OCEAN__PORT__CLIENT_ID={{ cookiecutter.port_client_id }}
2
+ OCEAN__PORT__CLIENT_SECRET={{ cookiecutter.port_client_secret }}
3
+ OCEAN__INTEGRATION__IDENTIFIER={{ cookiecutter.integration_slug }}
4
+ OCEAN__PORT__BASE_URL={% if cookiecutter.is_us_region %}https://api.us.getport.io{% else %}https://api.getport.io{% endif %}
5
+ OCEAN__EVENT_LISTENER__TYPE=POLLING
6
+ OCEAN__INITIALIZE_PORT_RESOURCES=true
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import Any
2
+ from typing import Any, Literal
3
3
  from urllib.parse import quote_plus
4
4
 
5
5
  import httpx
@@ -11,7 +11,8 @@ from port_ocean.clients.port.utils import (
11
11
  handle_status_code,
12
12
  PORT_HTTP_MAX_CONNECTIONS_LIMIT,
13
13
  )
14
- from port_ocean.core.models import Entity
14
+ from port_ocean.core.models import Entity, PortAPIErrorMessage
15
+ from starlette import status
15
16
 
16
17
 
17
18
  class EntityClientMixin:
@@ -29,7 +30,27 @@ class EntityClientMixin:
29
30
  request_options: RequestOptions,
30
31
  user_agent_type: UserAgentType | None = None,
31
32
  should_raise: bool = True,
32
- ) -> Entity | None:
33
+ ) -> Entity | None | Literal[False]:
34
+ """
35
+ This function upserts an entity into Port.
36
+
37
+ Usage:
38
+ ```python
39
+ upsertedEntity = await self.context.port_client.upsert_entity(
40
+ entity,
41
+ event.port_app_config.get_port_request_options(),
42
+ user_agent_type,
43
+ should_raise=False,
44
+ )
45
+ ```
46
+ :param entity: An Entity to be upserted
47
+ :param request_options: A dictionary specifying how to upsert the entity
48
+ :param user_agent_type: a UserAgentType specifying who is preforming the action
49
+ :param should_raise: A boolean specifying whether the error should be raised or handled silently
50
+ :return: [Entity] if the upsert occured successfully
51
+ :return: [None] will be returned if entity is using search identifier
52
+ :return: [False] will be returned if upsert failed because of unmet dependency
53
+ """
33
54
  validation_only = request_options["validation_only"]
34
55
  async with self.semaphore:
35
56
  logger.debug(
@@ -50,13 +71,21 @@ class EntityClientMixin:
50
71
  },
51
72
  extensions={"retryable": True},
52
73
  )
53
-
54
74
  if response.is_error:
55
75
  logger.error(
56
76
  f"Error {'Validating' if validation_only else 'Upserting'} "
57
77
  f"entity: {entity.identifier} of "
58
78
  f"blueprint: {entity.blueprint}"
59
79
  )
80
+ result = response.json()
81
+
82
+ if (
83
+ response.status_code == status.HTTP_404_NOT_FOUND
84
+ and not result.get("ok")
85
+ and result.get("error") == PortAPIErrorMessage.NOT_FOUND.value
86
+ ):
87
+ # Return false to differentiate from `result_entity.is_using_search_identifier`
88
+ return False
60
89
  handle_status_code(response, should_raise)
61
90
  result = response.json()
62
91
 
@@ -14,6 +14,7 @@ from typing import (
14
14
  from uuid import uuid4
15
15
 
16
16
  from loguru import logger
17
+ from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
17
18
  from pydispatch import dispatcher # type: ignore
18
19
  from werkzeug.local import LocalStack, LocalProxy
19
20
 
@@ -24,6 +25,7 @@ from port_ocean.exceptions.context import (
24
25
  )
25
26
  from port_ocean.utils.misc import get_time
26
27
 
28
+
27
29
  if TYPE_CHECKING:
28
30
  from port_ocean.core.handlers.port_app_config.models import (
29
31
  ResourceConfig,
@@ -50,6 +52,9 @@ class EventContext:
50
52
  _parent_event: Optional["EventContext"] = None
51
53
  _event_id: str = field(default_factory=lambda: str(uuid4()))
52
54
  _on_abort_callbacks: list[AbortCallbackFunction] = field(default_factory=list)
55
+ entity_topological_sorter: EntityTopologicalSorter = field(
56
+ default_factory=EntityTopologicalSorter
57
+ )
53
58
 
54
59
  def on_abort(self, func: AbortCallbackFunction) -> None:
55
60
  self._on_abort_callbacks.append(func)
@@ -129,6 +134,11 @@ async def event_context(
129
134
  ) -> AsyncIterator[EventContext]:
130
135
  parent = parent_override or _event_context_stack.top
131
136
  parent_attributes = parent.attributes if parent else {}
137
+ entity_topological_sorter = (
138
+ parent.entity_topological_sorter
139
+ if parent and parent.entity_topological_sorter
140
+ else EntityTopologicalSorter()
141
+ )
132
142
 
133
143
  attributes = {**parent_attributes, **(attributes or {})}
134
144
  new_event = EventContext(
@@ -138,6 +148,7 @@ async def event_context(
138
148
  _parent_event=parent,
139
149
  # inherit port app config from parent event, so it can be used in nested events
140
150
  _port_app_config=parent.port_app_config if parent else None,
151
+ entity_topological_sorter=entity_topological_sorter,
141
152
  )
142
153
  _event_context_stack.push(new_event)
143
154
 
@@ -14,7 +14,7 @@ from port_ocean.core.defaults.common import (
14
14
  )
15
15
  from port_ocean.core.handlers.port_app_config.models import PortAppConfig
16
16
  from port_ocean.core.models import Blueprint
17
- from port_ocean.core.utils import gather_and_split_errors_from_results
17
+ from port_ocean.core.utils.utils import gather_and_split_errors_from_results
18
18
  from port_ocean.exceptions.port_defaults import (
19
19
  AbortDefaultCreationError,
20
20
  )
@@ -8,12 +8,11 @@ from port_ocean.core.handlers.entities_state_applier.base import (
8
8
  from port_ocean.core.handlers.entities_state_applier.port.get_related_entities import (
9
9
  get_related_entities,
10
10
  )
11
- from port_ocean.core.handlers.entities_state_applier.port.order_by_entities_dependencies import (
12
- order_by_entities_dependencies,
13
- )
11
+
14
12
  from port_ocean.core.models import Entity
15
13
  from port_ocean.core.ocean_types import EntityDiff
16
- from port_ocean.core.utils import is_same_entity, get_port_diff
14
+ from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
15
+ from port_ocean.core.utils.utils import is_same_entity, get_port_diff
17
16
 
18
17
 
19
18
  class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
@@ -106,19 +105,7 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
106
105
  should_raise=False,
107
106
  )
108
107
  else:
109
- entities_with_search_identifier: list[Entity] = []
110
- entities_without_search_identifier: list[Entity] = []
111
108
  for entity in entities:
112
- if entity.is_using_search_identifier:
113
- entities_with_search_identifier.append(entity)
114
- else:
115
- entities_without_search_identifier.append(entity)
116
-
117
- ordered_created_entities = reversed(
118
- entities_with_search_identifier
119
- + order_by_entities_dependencies(entities_without_search_identifier)
120
- )
121
- for entity in ordered_created_entities:
122
109
  upsertedEntity = await self.context.port_client.upsert_entity(
123
110
  entity,
124
111
  event.port_app_config.get_port_request_options(),
@@ -127,6 +114,9 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
127
114
  )
128
115
  if upsertedEntity:
129
116
  modified_entities.append(upsertedEntity)
117
+ # condition to false to differentiate from `result_entity.is_using_search_identifier`
118
+ if upsertedEntity is False:
119
+ event.entity_topological_sorter.register_entity(entity)
130
120
  return modified_entities
131
121
 
132
122
  async def delete(
@@ -141,7 +131,9 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
141
131
  should_raise=False,
142
132
  )
143
133
  else:
144
- ordered_deleted_entities = order_by_entities_dependencies(entities)
134
+ ordered_deleted_entities = (
135
+ EntityTopologicalSorter.order_by_entities_dependencies(entities)
136
+ )
145
137
 
146
138
  for entity in ordered_deleted_entities:
147
139
  await self.context.port_client.delete_entity(
@@ -14,7 +14,6 @@ def node(entity: Entity) -> Node:
14
14
  def order_by_entities_dependencies(entities: list[Entity]) -> list[Entity]:
15
15
  nodes: dict[Node, Set[Node]] = {}
16
16
  entities_map = {}
17
-
18
17
  for entity in entities:
19
18
  nodes[node(entity)] = set()
20
19
  entities_map[node(entity)] = entity
@@ -33,7 +32,11 @@ def order_by_entities_dependencies(entities: list[Entity]) -> list[Entity]:
33
32
  ]
34
33
 
35
34
  for related_entity in related_entities:
36
- nodes[node(entity)].add(node(related_entity))
35
+ if (
36
+ entity.blueprint is not related_entity.blueprint
37
+ or entity.identifier is not related_entity.identifier
38
+ ):
39
+ nodes[node(entity)].add(node(related_entity))
37
40
 
38
41
  sort_op = TopologicalSorter(nodes)
39
42
  try:
@@ -16,7 +16,10 @@ from port_ocean.core.ocean_types import (
16
16
  EntitySelectorDiff,
17
17
  CalculationResult,
18
18
  )
19
- from port_ocean.core.utils import gather_and_split_errors_from_results, zip_and_sum
19
+ from port_ocean.core.utils.utils import (
20
+ gather_and_split_errors_from_results,
21
+ zip_and_sum,
22
+ )
20
23
  from port_ocean.exceptions.core import EntityProcessorException
21
24
  from port_ocean.utils.queue_utils import process_in_queue
22
25
 
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from graphlib import CycleError
2
3
  import inspect
3
4
  import typing
4
5
  from typing import Callable, Awaitable, Any
@@ -27,7 +28,7 @@ from port_ocean.core.ocean_types import (
27
28
  RAW_ITEM,
28
29
  CalculationResult,
29
30
  )
30
- from port_ocean.core.utils import zip_and_sum, gather_and_split_errors_from_results
31
+ from port_ocean.core.utils.utils import zip_and_sum, gather_and_split_errors_from_results
31
32
  from port_ocean.exceptions.core import OceanAbortException
32
33
 
33
34
  SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
@@ -396,7 +397,20 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
396
397
  {"before": entities_before_flatten, "after": entities_after_flatten},
397
398
  user_agent_type,
398
399
  )
399
-
400
+ async def sort_and_upsert_failed_entities(self,user_agent_type: UserAgentType)->None:
401
+ try:
402
+ if not event.entity_topological_sorter.should_execute():
403
+ return None
404
+ logger.info(f"Executings topological sort of {event.entity_topological_sorter.get_entities_count()} entities failed to upsert.",failed_toupsert_entities_count=event.entity_topological_sorter.get_entities_count())
405
+
406
+ for entity in event.entity_topological_sorter.get_entities():
407
+ await self.entities_state_applier.context.port_client.upsert_entity(entity,event.port_app_config.get_port_request_options(),user_agent_type,should_raise=False)
408
+
409
+ except OceanAbortException as ocean_abort:
410
+ logger.info(f"Failed topological sort of failed to upsert entites - trying to upsert unordered {event.entity_topological_sorter.get_entities_count()} entities.",failed_topological_sort_entities_count=event.entity_topological_sorter.get_entities_count() )
411
+ if isinstance(ocean_abort.__cause__,CycleError):
412
+ for entity in event.entity_topological_sorter.get_entities(False):
413
+ await self.entities_state_applier.context.port_client.upsert_entity(entity,event.port_app_config.get_port_request_options(),user_agent_type,should_raise=False)
400
414
  async def sync_raw_all(
401
415
  self,
402
416
  _: dict[Any, Any] | None = None,
@@ -426,6 +440,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
426
440
  use_cache=False
427
441
  )
428
442
  logger.info(f"Resync will use the following mappings: {app_config.dict()}")
443
+
429
444
  try:
430
445
  did_fetched_current_state = True
431
446
  entities_at_port = await ocean.port_client.search_entities(
@@ -455,6 +470,8 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
455
470
  event.on_abort(lambda: task.cancel())
456
471
 
457
472
  creation_results.append(await task)
473
+
474
+ await self.sort_and_upsert_failed_entities(user_agent_type)
458
475
  except asyncio.CancelledError as e:
459
476
  logger.warning("Resync aborted successfully, skipping delete phase. This leads to an incomplete state")
460
477
  raise
@@ -9,7 +9,7 @@ from port_ocean.core.ocean_types import (
9
9
  RESYNC_EVENT_LISTENER,
10
10
  RESYNC_RESULT,
11
11
  )
12
- from port_ocean.core.utils import validate_result
12
+ from port_ocean.core.utils.utils import validate_result
13
13
  from port_ocean.exceptions.core import (
14
14
  RawObjectValidationException,
15
15
  OceanAbortException,
port_ocean/core/models.py CHANGED
@@ -27,6 +27,10 @@ class Runtime(Enum):
27
27
  ) or installation_type == self.value
28
28
 
29
29
 
30
+ class PortAPIErrorMessage(Enum):
31
+ NOT_FOUND = "not_found"
32
+
33
+
30
34
  class Entity(BaseModel):
31
35
  identifier: Any
32
36
  blueprint: Any
@@ -0,0 +1,90 @@
1
+ from typing import Any, Generator
2
+ from port_ocean.context import event
3
+ from port_ocean.core.models import Entity
4
+
5
+ from loguru import logger
6
+
7
+ from graphlib import TopologicalSorter, CycleError
8
+ from typing import Set
9
+
10
+ from port_ocean.exceptions.core import OceanAbortException
11
+
12
+ Node = tuple[str, str]
13
+
14
+
15
+ class EntityTopologicalSorter:
16
+ def __init__(self) -> None:
17
+ self.entities: list[Entity] = []
18
+
19
+ def register_entity(
20
+ self,
21
+ entity: Entity,
22
+ ) -> None:
23
+ logger.debug(
24
+ f"Will retry upserting entity - {entity.identifier} at the end of resync"
25
+ )
26
+ self.entities.append(entity)
27
+
28
+ def should_execute(self) -> int:
29
+ return not event.event.port_app_config.create_missing_related_entities
30
+
31
+ def get_entities_count(self) -> int:
32
+ return len(self.entities)
33
+
34
+ def get_entities(self, sorted: bool = True) -> Generator[Entity, Any, None]:
35
+ if not sorted:
36
+ for entity in self.entities:
37
+ yield entity
38
+ return
39
+
40
+ sorted_and_mapped = EntityTopologicalSorter.order_by_entities_dependencies(
41
+ self.entities
42
+ )
43
+ for entity in sorted_and_mapped:
44
+ yield entity
45
+
46
+ @staticmethod
47
+ def node(entity: Entity) -> Node:
48
+ return entity.identifier, entity.blueprint
49
+
50
+ @staticmethod
51
+ def order_by_entities_dependencies(entities: list[Entity]) -> list[Entity]:
52
+ nodes: dict[Node, Set[Node]] = {}
53
+ entities_map = {}
54
+ for entity in entities:
55
+ nodes[EntityTopologicalSorter.node(entity)] = set()
56
+ entities_map[EntityTopologicalSorter.node(entity)] = entity
57
+
58
+ for entity in entities:
59
+ relation_target_ids: list[str] = sum(
60
+ [
61
+ identifiers if isinstance(identifiers, list) else [identifiers]
62
+ for identifiers in entity.relations.values()
63
+ if identifiers is not None
64
+ ],
65
+ [],
66
+ )
67
+ related_entities = [
68
+ related
69
+ for related in entities
70
+ if related.identifier in relation_target_ids
71
+ ]
72
+
73
+ for related_entity in related_entities:
74
+ if (
75
+ entity.blueprint is not related_entity.blueprint
76
+ or entity.identifier is not related_entity.identifier
77
+ ):
78
+ nodes[EntityTopologicalSorter.node(entity)].add(
79
+ EntityTopologicalSorter.node(related_entity)
80
+ )
81
+
82
+ sort_op = TopologicalSorter(nodes)
83
+ try:
84
+ return [entities_map[item] for item in sort_op.static_order()]
85
+ except CycleError as ex:
86
+ raise OceanAbortException(
87
+ "Cannot order entities due to cyclic dependencies. \n"
88
+ "If you do want to have cyclic dependencies, please make sure to set the keys"
89
+ " 'createMissingRelatedEntities' and 'deleteDependentEntities' in the integration config in Port."
90
+ ) from ex
@@ -0,0 +1,5 @@
1
+ from port_ocean.cli.commands.main import cli_start
2
+
3
+
4
+ if __name__ == "__main__":
5
+ cli_start()
port_ocean/run.py CHANGED
@@ -9,7 +9,7 @@ from port_ocean.bootstrap import create_default_app
9
9
  from port_ocean.config.dynamic import default_config_factory
10
10
  from port_ocean.config.settings import ApplicationSettings, LogLevelType
11
11
  from port_ocean.core.defaults.initialize import initialize_defaults
12
- from port_ocean.core.utils import validate_integration_runtime
12
+ from port_ocean.core.utils.utils import validate_integration_runtime
13
13
  from port_ocean.log.logger_setup import setup_logger
14
14
  from port_ocean.ocean import Ocean
15
15
  from port_ocean.utils.misc import get_spec_file, load_module
@@ -0,0 +1,400 @@
1
+ from contextlib import asynccontextmanager
2
+ from graphlib import CycleError
3
+ from typing import Any, AsyncGenerator
4
+
5
+ from httpx import Response
6
+ from port_ocean.clients.port.client import PortClient
7
+ from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
8
+ from port_ocean.exceptions.core import OceanAbortException
9
+ import pytest
10
+ from unittest.mock import MagicMock, AsyncMock, patch
11
+ from port_ocean.ocean import Ocean
12
+ from port_ocean.context.ocean import PortOceanContext
13
+ from port_ocean.core.handlers.port_app_config.models import (
14
+ EntityMapping,
15
+ MappingsConfig,
16
+ PortAppConfig,
17
+ PortResourceConfig,
18
+ ResourceConfig,
19
+ Selector,
20
+ )
21
+ from port_ocean.core.integrations.mixins import SyncRawMixin
22
+ from port_ocean.core.handlers.entities_state_applier.port.applier import (
23
+ HttpEntitiesStateApplier,
24
+ )
25
+ from port_ocean.core.handlers.entity_processor.jq_entity_processor import (
26
+ JQEntityProcessor,
27
+ )
28
+ from port_ocean.core.models import Entity
29
+ from port_ocean.context.event import EventContext, event_context, EventType
30
+ from port_ocean.clients.port.types import UserAgentType
31
+ from port_ocean.context.ocean import ocean
32
+
33
+
34
+ @pytest.fixture
35
+ def mock_port_client(mock_http_client: MagicMock) -> PortClient:
36
+ mock_port_client = PortClient(
37
+ MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
38
+ )
39
+ mock_port_client.auth = AsyncMock()
40
+ mock_port_client.auth.headers = AsyncMock(
41
+ return_value={
42
+ "Authorization": "test",
43
+ "User-Agent": "test",
44
+ }
45
+ )
46
+
47
+ mock_port_client.search_entities = AsyncMock(return_value=[]) # type: ignore
48
+ mock_port_client.client = mock_http_client
49
+ return mock_port_client
50
+
51
+
52
+ @pytest.fixture
53
+ def mock_http_client() -> MagicMock:
54
+ mock_http_client = MagicMock()
55
+ mock_upserted_entities = []
56
+
57
+ async def post(url: str, *args: Any, **kwargs: Any) -> Response:
58
+ entity = kwargs.get("json", {})
59
+ if entity.get("properties", {}).get("mock_is_to_fail", {}):
60
+ return Response(
61
+ 404, headers=MagicMock(), json={"ok": False, "error": "not_found"}
62
+ )
63
+
64
+ mock_upserted_entities.append(
65
+ f"{entity.get('identifier')}-{entity.get('blueprint')}"
66
+ )
67
+ return Response(
68
+ 200,
69
+ json={
70
+ "entity": {
71
+ "identifier": entity.get("identifier"),
72
+ "blueprint": entity.get("blueprint"),
73
+ }
74
+ },
75
+ )
76
+
77
+ mock_http_client.post = AsyncMock(side_effect=post)
78
+ return mock_http_client
79
+
80
+
81
+ @pytest.fixture
82
+ def mock_ocean(mock_port_client: PortClient) -> Ocean:
83
+ with patch("port_ocean.ocean.Ocean.__init__", return_value=None):
84
+ ocean_mock = Ocean(
85
+ MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
86
+ )
87
+ ocean_mock.config = MagicMock()
88
+ ocean_mock.config.port = MagicMock()
89
+ ocean_mock.config.port.port_app_config_cache_ttl = 60
90
+ ocean_mock.port_client = mock_port_client
91
+
92
+ return ocean_mock
93
+
94
+
95
+ @pytest.fixture
96
+ def mock_context(mock_ocean: Ocean) -> PortOceanContext:
97
+ context = PortOceanContext(mock_ocean)
98
+ ocean._app = context.app
99
+ return context
100
+
101
+
102
+ @pytest.fixture
103
+ def mock_port_app_config() -> PortAppConfig:
104
+ return PortAppConfig(
105
+ enable_merge_entity=True,
106
+ delete_dependent_entities=True,
107
+ create_missing_related_entities=False,
108
+ resources=[
109
+ ResourceConfig(
110
+ kind="project",
111
+ selector=Selector(query="true"),
112
+ port=PortResourceConfig(
113
+ entity=MappingsConfig(
114
+ mappings=EntityMapping(
115
+ identifier=".id | tostring",
116
+ title=".name",
117
+ blueprint='"service"',
118
+ properties={"url": ".web_url"},
119
+ relations={},
120
+ )
121
+ )
122
+ ),
123
+ )
124
+ ],
125
+ )
126
+
127
+
128
+ @pytest.fixture
129
+ def mock_port_app_config_handler(mock_port_app_config: PortAppConfig) -> MagicMock:
130
+ handler = MagicMock()
131
+
132
+ async def get_config(use_cache: bool = True) -> Any:
133
+ return mock_port_app_config
134
+
135
+ handler.get_port_app_config = get_config
136
+ return handler
137
+
138
+
139
+ @pytest.fixture
140
+ def mock_entity_processor(mock_context: PortOceanContext) -> JQEntityProcessor:
141
+ return JQEntityProcessor(mock_context)
142
+
143
+
144
+ @pytest.fixture
145
+ def mock_entities_state_applier(
146
+ mock_context: PortOceanContext,
147
+ ) -> HttpEntitiesStateApplier:
148
+ return HttpEntitiesStateApplier(mock_context)
149
+
150
+
151
+ @pytest.fixture
152
+ def mock_sync_raw_mixin(
153
+ mock_entity_processor: JQEntityProcessor,
154
+ mock_entities_state_applier: HttpEntitiesStateApplier,
155
+ mock_port_app_config_handler: MagicMock,
156
+ ) -> SyncRawMixin:
157
+ sync_raw_mixin = SyncRawMixin()
158
+ sync_raw_mixin._entity_processor = mock_entity_processor
159
+ sync_raw_mixin._entities_state_applier = mock_entities_state_applier
160
+ sync_raw_mixin._port_app_config_handler = mock_port_app_config_handler
161
+ sync_raw_mixin._get_resource_raw_results = AsyncMock(return_value=([{}], [])) # type: ignore
162
+ sync_raw_mixin._entity_processor.parse_items = AsyncMock(return_value=MagicMock()) # type: ignore
163
+
164
+ return sync_raw_mixin
165
+
166
+
167
+ @asynccontextmanager
168
+ async def no_op_event_context(
169
+ existing_event: EventContext,
170
+ ) -> AsyncGenerator[EventContext, None]:
171
+ yield existing_event
172
+
173
+
174
+ def create_entity(
175
+ id: str, blueprint: str, relation: dict[str, str], is_to_fail: bool
176
+ ) -> Entity:
177
+ entity = Entity(identifier=id, blueprint=blueprint)
178
+ entity.relations = relation
179
+ entity.properties = {"mock_is_to_fail": is_to_fail}
180
+ return entity
181
+
182
+
183
+ @pytest.mark.asyncio
184
+ async def test_sync_raw_mixin_self_dependency(
185
+ mock_sync_raw_mixin: SyncRawMixin,
186
+ ) -> None:
187
+ entities_params = [
188
+ ("entity_1", "service", {"service": "entity_1"}, True),
189
+ ("entity_2", "service", {"service": "entity_2"}, False),
190
+ ]
191
+ entities = [create_entity(*entity_param) for entity_param in entities_params]
192
+
193
+ calc_result_mock = MagicMock()
194
+ calc_result_mock.entity_selector_diff.passed = entities
195
+ calc_result_mock.errors = []
196
+
197
+ mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
198
+
199
+ mock_order_by_entities_dependencies = MagicMock(
200
+ side_effect=EntityTopologicalSorter.order_by_entities_dependencies
201
+ )
202
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
203
+ app_config = (
204
+ await mock_sync_raw_mixin.port_app_config_handler.get_port_app_config(
205
+ use_cache=False
206
+ )
207
+ )
208
+ event.port_app_config = app_config
209
+ event.entity_topological_sorter.register_entity = MagicMock(side_effect=event.entity_topological_sorter.register_entity) # type: ignore
210
+ event.entity_topological_sorter.get_entities = MagicMock(side_effect=event.entity_topological_sorter.get_entities) # type: ignore
211
+
212
+ with patch(
213
+ "port_ocean.core.integrations.mixins.sync_raw.event_context",
214
+ lambda *args, **kwargs: no_op_event_context(event),
215
+ ):
216
+ with patch(
217
+ "port_ocean.core.utils.entity_topological_sorter.EntityTopologicalSorter.order_by_entities_dependencies",
218
+ mock_order_by_entities_dependencies,
219
+ ):
220
+
221
+ await mock_sync_raw_mixin.sync_raw_all(
222
+ trigger_type="machine", user_agent_type=UserAgentType.exporter
223
+ )
224
+
225
+ assert (
226
+ len(event.entity_topological_sorter.entities) == 1
227
+ ), "Expected one failed entity callback due to retry logic"
228
+ assert event.entity_topological_sorter.register_entity.call_count == 1
229
+ assert event.entity_topological_sorter.get_entities.call_count == 1
230
+
231
+ assert mock_order_by_entities_dependencies.call_count == 1
232
+ assert [
233
+ call[0][0][0]
234
+ for call in mock_order_by_entities_dependencies.call_args_list
235
+ ] == [entity for entity in entities if entity.identifier == "entity_1"]
236
+
237
+
238
+ @pytest.mark.asyncio
239
+ async def test_sync_raw_mixin_circular_dependency(
240
+ mock_sync_raw_mixin: SyncRawMixin, mock_ocean: Ocean
241
+ ) -> None:
242
+ entities_params = [
243
+ ("entity_1", "service", {"service": "entity_2"}, True),
244
+ ("entity_2", "service", {"service": "entity_1"}, True),
245
+ ]
246
+ entities = [create_entity(*entity_param) for entity_param in entities_params]
247
+
248
+ calc_result_mock = MagicMock()
249
+ calc_result_mock.entity_selector_diff.passed = entities
250
+ calc_result_mock.errors = []
251
+
252
+ mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
253
+
254
+ mock_order_by_entities_dependencies = MagicMock(
255
+ side_effect=EntityTopologicalSorter.order_by_entities_dependencies
256
+ )
257
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
258
+ app_config = (
259
+ await mock_sync_raw_mixin.port_app_config_handler.get_port_app_config(
260
+ use_cache=False
261
+ )
262
+ )
263
+ event.port_app_config = app_config
264
+ org = event.entity_topological_sorter.register_entity
265
+
266
+ def mock_register_entity(*args: Any, **kwargs: Any) -> Any:
267
+ entity = args[0]
268
+ entity.properties["mock_is_to_fail"] = False
269
+ return org(*args, **kwargs)
270
+
271
+ event.entity_topological_sorter.register_entity = MagicMock(side_effect=mock_register_entity) # type: ignore
272
+ raiesed_error_handle_failed = []
273
+ org_get_entities = event.entity_topological_sorter.get_entities
274
+
275
+ def handle_failed_wrapper(*args: Any, **kwargs: Any) -> Any:
276
+ try:
277
+ return list(org_get_entities(*args, **kwargs))
278
+ except Exception as e:
279
+ raiesed_error_handle_failed.append(e)
280
+ raise e
281
+
282
+ event.entity_topological_sorter.get_entities = MagicMock(side_effect=lambda *args, **kwargs: handle_failed_wrapper(*args, **kwargs)) # type: ignore
283
+
284
+ with patch(
285
+ "port_ocean.core.integrations.mixins.sync_raw.event_context",
286
+ lambda *args, **kwargs: no_op_event_context(event),
287
+ ):
288
+ with patch(
289
+ "port_ocean.core.utils.entity_topological_sorter.EntityTopologicalSorter.order_by_entities_dependencies",
290
+ mock_order_by_entities_dependencies,
291
+ ):
292
+
293
+ await mock_sync_raw_mixin.sync_raw_all(
294
+ trigger_type="machine", user_agent_type=UserAgentType.exporter
295
+ )
296
+
297
+ assert (
298
+ len(event.entity_topological_sorter.entities) == 2
299
+ ), "Expected one failed entity callback due to retry logic"
300
+ assert event.entity_topological_sorter.register_entity.call_count == 2
301
+ assert event.entity_topological_sorter.get_entities.call_count == 2
302
+ assert [
303
+ call[0]
304
+ for call in event.entity_topological_sorter.get_entities.call_args_list
305
+ ] == [(), (False,)]
306
+ assert len(raiesed_error_handle_failed) == 1
307
+ assert isinstance(raiesed_error_handle_failed[0], OceanAbortException)
308
+ assert isinstance(raiesed_error_handle_failed[0].__cause__, CycleError)
309
+ assert (
310
+ len(mock_ocean.port_client.client.post.call_args_list) # type: ignore
311
+ / len(entities)
312
+ == 2
313
+ )
314
+
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_sync_raw_mixin_dependency(
318
+ mock_sync_raw_mixin: SyncRawMixin, mock_ocean: Ocean
319
+ ) -> None:
320
+ entities_params = [
321
+ ("entity_1", "service", {"service": "entity_3"}, True),
322
+ ("entity_2", "service", {"service": "entity_4"}, True),
323
+ ("entity_3", "service", {"service": ""}, True),
324
+ ("entity_4", "service", {"service": "entity_3"}, True),
325
+ ("entity_5", "service", {"service": "entity_1"}, True),
326
+ ]
327
+ entities = [create_entity(*entity_param) for entity_param in entities_params]
328
+
329
+ calc_result_mock = MagicMock()
330
+ calc_result_mock.entity_selector_diff.passed = entities
331
+ calc_result_mock.errors = []
332
+
333
+ mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
334
+
335
+ mock_order_by_entities_dependencies = MagicMock(
336
+ side_effect=EntityTopologicalSorter.order_by_entities_dependencies
337
+ )
338
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
339
+ app_config = (
340
+ await mock_sync_raw_mixin.port_app_config_handler.get_port_app_config(
341
+ use_cache=False
342
+ )
343
+ )
344
+ event.port_app_config = app_config
345
+ org = event.entity_topological_sorter.register_entity
346
+
347
+ def mock_register_entity(*args: Any, **kwargs: Any) -> None:
348
+ entity = args[0]
349
+ entity.properties["mock_is_to_fail"] = False
350
+ return org(*args, **kwargs)
351
+
352
+ event.entity_topological_sorter.register_entity = MagicMock(side_effect=mock_register_entity) # type: ignore
353
+ raiesed_error_handle_failed = []
354
+ org_event_get_entities = event.entity_topological_sorter.get_entities
355
+
356
+ def get_entities_wrapper(*args: Any, **kwargs: Any) -> Any:
357
+ try:
358
+ return org_event_get_entities(*args, **kwargs)
359
+ except Exception as e:
360
+ raiesed_error_handle_failed.append(e)
361
+ raise e
362
+
363
+ event.entity_topological_sorter.get_entities = MagicMock(side_effect=lambda *args, **kwargs: get_entities_wrapper(*args, **kwargs)) # type: ignore
364
+
365
+ with patch(
366
+ "port_ocean.core.integrations.mixins.sync_raw.event_context",
367
+ lambda *args, **kwargs: no_op_event_context(event),
368
+ ):
369
+ with patch(
370
+ "port_ocean.core.utils.entity_topological_sorter.EntityTopologicalSorter.order_by_entities_dependencies",
371
+ mock_order_by_entities_dependencies,
372
+ ):
373
+
374
+ await mock_sync_raw_mixin.sync_raw_all(
375
+ trigger_type="machine", user_agent_type=UserAgentType.exporter
376
+ )
377
+
378
+ assert event.entity_topological_sorter.register_entity.call_count == 5
379
+ assert (
380
+ len(event.entity_topological_sorter.entities) == 5
381
+ ), "Expected one failed entity callback due to retry logic"
382
+ assert event.entity_topological_sorter.get_entities.call_count == 1
383
+ assert len(raiesed_error_handle_failed) == 0
384
+ assert mock_ocean.port_client.client.post.call_count == 10 # type: ignore
385
+ assert mock_order_by_entities_dependencies.call_count == 1
386
+
387
+ first = mock_ocean.port_client.client.post.call_args_list[0:5] # type: ignore
388
+ second = mock_ocean.port_client.client.post.call_args_list[5:10] # type: ignore
389
+
390
+ assert "-".join(
391
+ [call[1].get("json").get("identifier") for call in first]
392
+ ) == "-".join([entity.identifier for entity in entities])
393
+ assert "-".join(
394
+ [call[1].get("json").get("identifier") for call in second]
395
+ ) in (
396
+ "entity_3-entity_4-entity_1-entity_2-entity_5",
397
+ "entity_3-entity_4-entity_1-entity_5-entity_2",
398
+ "entity_3-entity_1-entity_4-entity_2-entity_5",
399
+ "entity_3-entity_1-entity_4-entity_5-entity_2",
400
+ )
@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch
2
2
 
3
3
  import pytest
4
4
 
5
- from port_ocean.core.utils import validate_integration_runtime
5
+ from port_ocean.core.utils.utils import validate_integration_runtime
6
6
  from port_ocean.clients.port.client import PortClient
7
7
  from port_ocean.core.models import Runtime
8
8
  from port_ocean.tests.helpers.port_client import get_port_client_for_integration
@@ -0,0 +1,99 @@
1
+ from port_ocean.core.models import Entity
2
+ from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
3
+ from unittest.mock import MagicMock
4
+ from port_ocean.exceptions.core import (
5
+ OceanAbortException,
6
+ )
7
+
8
+
9
+ def create_entity(
10
+ identifier: str, buleprint: str, dependencies: dict[str, str] = {}
11
+ ) -> Entity:
12
+ entity = MagicMock()
13
+ entity.identifier = identifier
14
+ entity.blueprint = buleprint
15
+ entity.relations = dependencies or {}
16
+ return entity
17
+
18
+
19
+ def test_handle_failed_with_dependencies() -> None:
20
+ # processed_order:list[str] = []
21
+ entity_a = create_entity(
22
+ "entity_a",
23
+ "buleprint_a",
24
+ ) # No dependencies
25
+ entity_b = create_entity(
26
+ "entity_b", "buleprint_a", {"dep_name_1": "entity_a"}
27
+ ) # Depends on entity_a
28
+ entity_c = create_entity(
29
+ "entity_c", "buleprint_b", {"dep_name_2": "entity_b"}
30
+ ) # Depends on entity_b
31
+
32
+ entity_topological_sort = EntityTopologicalSorter()
33
+ # Register fails with unsorted order
34
+ entity_topological_sort.register_entity(entity_c)
35
+ entity_topological_sort.register_entity(entity_a)
36
+ entity_topological_sort.register_entity(entity_b)
37
+
38
+ processed_order = [
39
+ f"{entity.identifier}-{entity.blueprint}"
40
+ for entity in list(entity_topological_sort.get_entities())
41
+ ]
42
+ assert processed_order == [
43
+ "entity_a-buleprint_a",
44
+ "entity_b-buleprint_a",
45
+ "entity_c-buleprint_b",
46
+ ], f"Processed order: {processed_order}"
47
+
48
+
49
+ def test_handle_failed_with_self_dependencies() -> None:
50
+ entity_a = create_entity(
51
+ "entity_a", "buleprint_a", {"dep_name_1": "entity_a"}
52
+ ) # Self dependency
53
+ entity_b = create_entity(
54
+ "entity_b", "buleprint_a", {"dep_name_1": "entity_a"}
55
+ ) # Depends on entity_a
56
+ entity_c = create_entity(
57
+ "entity_c", "buleprint_b", {"dep_name_2": "entity_b"}
58
+ ) # Depends on entity_b
59
+
60
+ entity_topological_sort = EntityTopologicalSorter()
61
+
62
+ # Register fails with unsorted order
63
+ entity_topological_sort.register_entity(entity_c)
64
+ entity_topological_sort.register_entity(entity_a)
65
+ entity_topological_sort.register_entity(entity_b)
66
+
67
+ processed_order = [
68
+ f"{entity.identifier}-{entity.blueprint}"
69
+ for entity in list(entity_topological_sort.get_entities())
70
+ ]
71
+
72
+ assert processed_order == [
73
+ "entity_a-buleprint_a",
74
+ "entity_b-buleprint_a",
75
+ "entity_c-buleprint_b",
76
+ ], f"Processed order: {processed_order}"
77
+
78
+
79
+ def test_handle_failed_with_circular_dependencies() -> None:
80
+ # processed_order:list[str] = []
81
+ entity_a = create_entity(
82
+ "entity_a", "buleprint_a", {"dep_name_1": "entity_b"}
83
+ ) # Self dependency
84
+ entity_b = create_entity(
85
+ "entity_b", "buleprint_a", {"dep_name_1": "entity_a"}
86
+ ) # Depends on entity_a
87
+
88
+ entity_topological_sort = EntityTopologicalSorter()
89
+ try:
90
+ entity_topological_sort.register_entity(entity_a)
91
+ entity_topological_sort.register_entity(entity_b)
92
+ entity_topological_sort.get_entities()
93
+
94
+ except OceanAbortException as e:
95
+ assert isinstance(e, OceanAbortException)
96
+ assert (
97
+ e.args[0]
98
+ == "Cannot order entities due to cyclic dependencies. \nIf you do want to have cyclic dependencies, please make sure to set the keys 'createMissingRelatedEntities' and 'deleteDependentEntities' in the integration config in Port."
99
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.15.2
3
+ Version: 0.16.0
4
4
  Summary: Port Ocean is a CLI tool for managing your Port projects.
5
5
  Home-page: https://app.getport.io
6
6
  Keywords: ocean,port-ocean,port
@@ -17,15 +17,15 @@ port_ocean/cli/commands/defaults/dock.py,sha256=pFtHrU_LTvb5Ddrzj09Wxy-jg1Ym10wB
17
17
  port_ocean/cli/commands/defaults/group.py,sha256=hii_4CYoQ7jSMePbnP4AmruO_RKWCUcoV7dXXBlZafc,115
18
18
  port_ocean/cli/commands/list_integrations.py,sha256=DVVioFruGUE-_v6UUHlcemWNN6RlWwCrf1X4HmAXsf8,1134
19
19
  port_ocean/cli/commands/main.py,sha256=gj0lmuLep2XeLNuabB7Wk0UVYPT7_CD_rAw5AoUQWSE,1057
20
- port_ocean/cli/commands/new.py,sha256=39_RnEZHoY0pWf59z_oXrLXAyVwzmFY0XcpCJrtZINI,3496
20
+ port_ocean/cli/commands/new.py,sha256=qJ6fQG1t95U574Cvx5pMRgxabrmdB6kxEI0bwuTryb8,3262
21
21
  port_ocean/cli/commands/pull.py,sha256=VvrRjLNlfPuLIf7KzeIcbzzdi98Z0M9wCRpXC3QPxdI,2306
22
22
  port_ocean/cli/commands/sail.py,sha256=rY7rEMjfy_KXiWvtL0T72TTLgeQ3HW4SOzKkz9wL9nI,2282
23
23
  port_ocean/cli/commands/version.py,sha256=hEuIEIcm6Zkamz41Z9nxeSM_4g3oNlAgWwQyDGboh-E,536
24
24
  port_ocean/cli/cookiecutter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- port_ocean/cli/cookiecutter/cookiecutter.json,sha256=N5UrAP2e5JbgEDz_WTQFIZlzSveME6x32sHeA7idjh0,481
25
+ port_ocean/cli/cookiecutter/cookiecutter.json,sha256=ie-LJjg-ek3lP2RRosY2u_q2W4y2TykXm_Gynjjt6Es,814
26
26
  port_ocean/cli/cookiecutter/extensions.py,sha256=eQNjZvy2enDkJpvMbBGil77Xk9-38f862wfnmCjdoBc,446
27
27
  port_ocean/cli/cookiecutter/hooks/post_gen_project.py,sha256=tFqtsjSbu7HMN32WIiFO37S1a_dfHezvdPwmM6MmNJk,1182
28
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example,sha256=LnNPRe3RnzjWPL4tNLYEQiMvFEZHSy3ceqwQEapcpwE,92
28
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example,sha256=ywAmZto6YBGXyhEmpG1uYsgaHr2N1ZBRjdtRNt6Vkpw,388
29
29
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore,sha256=32p1lDW_g5hyBz486GWfDeR9m7ikFlASVri5a8vmNoo,2698
30
30
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore,sha256=kCpRPdl3S_jqYYZaOrc0-xa6-l3KqVjNRXc6jCkd_-Q,12
31
31
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json,sha256=9kf5gY4YjP78vEPfd9j7347sV6wiqeHzmBz7UJkvmDg,1187
@@ -49,7 +49,7 @@ port_ocean/clients/port/authentication.py,sha256=6-uDMWsJ0xLe1-9IoYXHWmwtufj8rJR
49
49
  port_ocean/clients/port/client.py,sha256=Xd8Jk25Uh4WXY_WW-z1Qbv6F3ZTBFPoOolsxHMfozKw,3366
50
50
  port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  port_ocean/clients/port/mixins/blueprints.py,sha256=POBl4uDocrgJBw4rvCAzwRcD4jk-uBL6pDAuKMTajdg,4633
52
- port_ocean/clients/port/mixins/entities.py,sha256=CnSU3dw1RTZUWYBTLP3KP7CyMweNcoDi1OlSWBwjXWU,8894
52
+ port_ocean/clients/port/mixins/entities.py,sha256=zVOltaiX3Hx8k_CLjBZ5QFH0r_fQOGMimJ6jAJf9jwI,10327
53
53
  port_ocean/clients/port/mixins/integrations.py,sha256=t8OSa7Iopnpp8IOEcp3a7WgwOcJEBdFow9UbGDKWxKI,4858
54
54
  port_ocean/clients/port/mixins/migrations.py,sha256=A6896oJF6WbFL2WroyTkMzr12yhVyWqGoq9dtLNSKBY,1457
55
55
  port_ocean/clients/port/retry_transport.py,sha256=PtIZOAZ6V-ncpVysRUsPOgt8Sf01QLnTKB5YeKBxkJk,1861
@@ -62,14 +62,14 @@ port_ocean/config/settings.py,sha256=cxGnOTO9cEwwFcTSzOmwMw1yPkQHniVJFyCQCyEZ6yY
62
62
  port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
64
64
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
- port_ocean/context/event.py,sha256=WduGbCPgm2J2a63EY4J3XWwFGSt3ja1acBVpyI_ciMo,5430
65
+ port_ocean/context/event.py,sha256=tf254jMqBW1GBmYDhfXMCkOqHA7C_chaYp1OY3Dfnsg,5869
66
66
  port_ocean/context/ocean.py,sha256=2EreWOj-N2H7QUjEt5wGiv5KHP4pTZc70tn_wHcpF4w,4657
67
67
  port_ocean/context/resource.py,sha256=yDj63URzQelj8zJPh4BAzTtPhpKr9Gw9DRn7I_0mJ1s,1692
68
68
  port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
69
  port_ocean/core/defaults/__init__.py,sha256=8qCZg8n06WAdMu9s_FiRtDYLGPGHbOuS60vapeUoAks,142
70
70
  port_ocean/core/defaults/clean.py,sha256=TOVe5b5FAjFspAkQuKA70k2BClCEFbrQ3xgiAoKXKYE,2427
71
71
  port_ocean/core/defaults/common.py,sha256=zJsj7jvlqIMLGXhdASUlbKS8GIAf-FDKKB0O7jB6nx0,4166
72
- port_ocean/core/defaults/initialize.py,sha256=M1EXgfbnrvP5e3f9or8Bi0-4zXECznfRfy7sJn2Gc14,8471
72
+ port_ocean/core/defaults/initialize.py,sha256=eX5AMo3fug202lHvnwwGAuYlVbh3yDvUQaimTzre1do,8477
73
73
  port_ocean/core/event_listener/__init__.py,sha256=mzJ33wRq0kh60fpVdOHVmvMTUQIvz3vxmifyBgwDn0E,889
74
74
  port_ocean/core/event_listener/base.py,sha256=1Nmpg00OfT2AD2L8eFm4VQEcdG2TClpSWJMhWhAjkEE,2356
75
75
  port_ocean/core/event_listener/factory.py,sha256=AYYfSHPAF7P5H-uQECXT0JVJjKDHrYkWJJBSL4mGkg8,3697
@@ -82,12 +82,12 @@ port_ocean/core/handlers/base.py,sha256=cTarblazu8yh8xz2FpB-dzDKuXxtoi143XJgPbV_
82
82
  port_ocean/core/handlers/entities_state_applier/__init__.py,sha256=kgLZDCeCEzi4r-0nzW9k78haOZNf6PX7mJOUr34A4c8,173
83
83
  port_ocean/core/handlers/entities_state_applier/base.py,sha256=5wHL0icfFAYRPqk8iV_wN49GdJ3aRUtO8tumSxBi4Wo,2268
84
84
  port_ocean/core/handlers/entities_state_applier/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
- port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=EirgWhT_TNeEwfdCElEDGkJ2tSOz9HsaUJ1i2uD7z28,5922
85
+ port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=FSJ77o-lilv5tF5WgpPNjA5lAw13CseFilL-kMHIF3A,5564
86
86
  port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py,sha256=1zncwCbE-Gej0xaWKlzZgoXxOBe9bgs_YxlZ8QW3NdI,1751
87
- port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=82BvU8t5w9uhsxX8hbnwuRPuWhW3cMeuT_5sVIkip1I,1550
87
+ port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=lyv6xKzhYfd6TioUgR3AVRSJqj7JpAaj1LxxU2xAqeo,1720
88
88
  port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybem9MthOs7p1Wawac87uSXz9U8,156
89
89
  port_ocean/core/handlers/entity_processor/base.py,sha256=udR0w5TstTOS5xOfTjAZIEdldn4xr6Oyb3DylatYX3Q,1869
90
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=EHxU5PxvGxKJn5gKRO01bMR9PmXM6NC_NZ1L2xqlQsI,8868
90
+ port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=X0aYDqgvuZG813JM1qjWpK-V9ASgctTRNgz02wJ4vMY,8887
91
91
  port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iEgEeXtn2pRMrT4Dze5r1Ixbk,134
92
92
  port_ocean/core/handlers/port_app_config/api.py,sha256=6VbKPwFzsWG0IYsVD81hxSmfqtHUFqrfUuj1DBX5g4w,853
93
93
  port_ocean/core/handlers/port_app_config/base.py,sha256=4Nxt2g8voEIHJ4Y1Km5NJcaG2iSbCklw5P8-Kus7Y9k,3007
@@ -100,11 +100,13 @@ port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4
100
100
  port_ocean/core/integrations/mixins/events.py,sha256=Ddfx2L4FpghV38waF8OfVeOV0bHBxNIgjU-q5ffillI,2341
101
101
  port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
102
102
  port_ocean/core/integrations/mixins/sync.py,sha256=B9fEs8faaYLLikH9GBjE_E61vo0bQDjIGQsQ1SRXOlA,3931
103
- port_ocean/core/integrations/mixins/sync_raw.py,sha256=BGS5EnZ2N3ifcAi94Wo-ZassSJ-_Se9eFJMpBDT7pNY,18841
104
- port_ocean/core/integrations/mixins/utils.py,sha256=7y1rGETZIjOQadyIjFJXIHKkQFKx_SwiP-TrAIsyyLY,2303
105
- port_ocean/core/models.py,sha256=71QIFHl-p401h2HnSDQ-aaLXhu6z3iHTwCBI0TewJos,1902
103
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=FjmYIP-7fqQY9tlqN9tNyHwdN81Dn4sqxVMI6JJbRYE,20275
104
+ port_ocean/core/integrations/mixins/utils.py,sha256=oN4Okz6xlaefpid1_Pud8HPSw9BwwjRohyNsknq-Myg,2309
105
+ port_ocean/core/models.py,sha256=O8nOKc4ORZz9tS5s6y5YgGLEBroXpvSPDqKuz48uKvs,1965
106
106
  port_ocean/core/ocean_types.py,sha256=3_d8-n626f1kWLQ_Jxw194LEyrOVupz05qs_Y1pvB-A,990
107
- port_ocean/core/utils.py,sha256=QSRuF9wlhbOw6cELlDlek_UIX6ciIuKWml8QhBmHU_k,3703
107
+ port_ocean/core/utils/entity_topological_sorter.py,sha256=MDUjM6OuDy4Xj68o-7InNN0w1jqjxeDfeY8U02vySNI,3081
108
+ port_ocean/core/utils/utils.py,sha256=QSRuF9wlhbOw6cELlDlek_UIX6ciIuKWml8QhBmHU_k,3703
109
+ port_ocean/debug_cli.py,sha256=gHrv-Ey3cImKOcGZpjoHlo4pa_zfmyOl6TUM4o9VtcA,96
108
110
  port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
109
111
  port_ocean/exceptions/api.py,sha256=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
110
112
  port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
@@ -123,14 +125,16 @@ port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2
123
125
  port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
124
126
  port_ocean/ocean.py,sha256=XxO-aRExs1hcy6aJY_nceu-QXRWB2ZLpkIPPuBkp-bQ,5247
125
127
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
126
- port_ocean/run.py,sha256=rTxBlrQd4yyrtgErCFJCHCEHs7d1OXrRiJehUYmIbN0,2212
128
+ port_ocean/run.py,sha256=YnqchtVI6diuc6HcyqjcKVqcO0PYnfYjVKVcwKeNO2E,2218
127
129
  port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
128
130
  port_ocean/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
129
131
  port_ocean/tests/clients/port/mixins/test_entities.py,sha256=A9myrnkLhKSQrnOLv1Zz2wiOVSxW65Q9RIUIRbn_V7w,1586
130
132
  port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
131
133
  port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
132
134
  port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=Yv03P-LDcJCKZ21exiTFrcT1eu0zn6Z954dilxrb52Y,10842
133
- port_ocean/tests/core/test_utils.py,sha256=94940TerN38jy81ebLJ2Fzf5JJUaV9krnce75APCwmM,2641
135
+ port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=RPrbw4Zs6bmhL9zMQviq7-qMfgP5_4nJDkfZiAukK-g,15782
136
+ port_ocean/tests/core/test_utils.py,sha256=Z3kdhb5V7Svhcyy3EansdTpgHL36TL6erNtU-OPwAcI,2647
137
+ port_ocean/tests/core/utils/test_entity_topological_sorter.py,sha256=zuq5WSPy_88PemG3mOUIHTxWMR_js1R7tOzUYlgBd68,3447
134
138
  port_ocean/tests/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
139
  port_ocean/tests/helpers/fake_port_api.py,sha256=9rtjC6iTQMfzWK6WipkDzzG0b1IIaRmvdJLOyV613vE,6479
136
140
  port_ocean/tests/helpers/fixtures.py,sha256=IQEplbHhRgjrAsZlnXrgSYA5YQEn25I9HgO3_Fjibxg,1481
@@ -152,8 +156,8 @@ port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,32
152
156
  port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
153
157
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
154
158
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
155
- port_ocean-0.15.2.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
156
- port_ocean-0.15.2.dist-info/METADATA,sha256=xiclpeTv7njo_QsJHJVG3NF6g-PSh74WwovQQYjwX6w,6673
157
- port_ocean-0.15.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
158
- port_ocean-0.15.2.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
159
- port_ocean-0.15.2.dist-info/RECORD,,
159
+ port_ocean-0.16.0.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
160
+ port_ocean-0.16.0.dist-info/METADATA,sha256=IonSZuh115163Dk9ISkmRGEHSnavV_0i58YgSuz2mek,6673
161
+ port_ocean-0.16.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
162
+ port_ocean-0.16.0.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
163
+ port_ocean-0.16.0.dist-info/RECORD,,
File without changes