port-ocean 0.15.3__py3-none-any.whl → 0.16.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.

@@ -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
 
@@ -31,6 +34,7 @@ class MappedEntity:
31
34
  entity: dict[str, Any] = field(default_factory=dict)
32
35
  did_entity_pass_selector: bool = False
33
36
  raw_data: Optional[dict[str, Any]] = None
37
+ misconfigurations: dict[str, str] = field(default_factory=dict)
34
38
 
35
39
 
36
40
  class JQEntityProcessor(BaseEntityProcessor):
@@ -92,21 +96,37 @@ class JQEntityProcessor(BaseEntityProcessor):
92
96
  )
93
97
 
94
98
  async def _search_as_object(
95
- self, data: dict[str, Any], obj: dict[str, Any]
99
+ self,
100
+ data: dict[str, Any],
101
+ obj: dict[str, Any],
102
+ misconfigurations: dict[str, str] | None = None,
96
103
  ) -> dict[str, Any | None]:
104
+ """
105
+ Identify and extract the relevant value for the chosen key and populate it into the entity
106
+ :param data: the property itself that holds the key and the value, it is being passed to the task and we get back a task item,
107
+ if the data is a dict, we will recursively call this function again.
108
+ :param obj: the key that we want its value to be mapped into our entity.
109
+ :param misconfigurations: due to the recursive nature of this function,
110
+ we aim to have a dict that represents all of the misconfigured properties and when used recursively,
111
+ we pass this reference to misfoncigured object to add the relevant misconfigured keys.
112
+ :return: Mapped object with found value.
113
+ """
114
+
97
115
  search_tasks: dict[
98
116
  str, Task[dict[str, Any | None]] | list[Task[dict[str, Any | None]]]
99
117
  ] = {}
100
118
  for key, value in obj.items():
101
119
  if isinstance(value, list):
102
120
  search_tasks[key] = [
103
- asyncio.create_task(self._search_as_object(data, obj))
121
+ asyncio.create_task(
122
+ self._search_as_object(data, obj, misconfigurations)
123
+ )
104
124
  for obj in value
105
125
  ]
106
126
 
107
127
  elif isinstance(value, dict):
108
128
  search_tasks[key] = asyncio.create_task(
109
- self._search_as_object(data, value)
129
+ self._search_as_object(data, value, misconfigurations)
110
130
  )
111
131
  else:
112
132
  search_tasks[key] = asyncio.create_task(self._search(data, value))
@@ -115,12 +135,20 @@ class JQEntityProcessor(BaseEntityProcessor):
115
135
  for key, task in search_tasks.items():
116
136
  try:
117
137
  if isinstance(task, list):
118
- result[key] = [await task for task in task]
138
+ result_list = []
139
+ for task in task:
140
+ task_result = await task
141
+ if task_result is None and misconfigurations is not None:
142
+ misconfigurations[key] = obj[key]
143
+ result_list.append(task_result)
144
+ result[key] = result_list
119
145
  else:
120
- result[key] = await task
146
+ task_result = await task
147
+ if task_result is None and misconfigurations is not None:
148
+ misconfigurations[key] = obj[key]
149
+ result[key] = task_result
121
150
  except Exception:
122
151
  result[key] = None
123
-
124
152
  return result
125
153
 
126
154
  async def _get_mapped_entity(
@@ -132,11 +160,15 @@ class JQEntityProcessor(BaseEntityProcessor):
132
160
  ) -> MappedEntity:
133
161
  should_run = await self._search_as_bool(data, selector_query)
134
162
  if parse_all or should_run:
135
- mapped_entity = await self._search_as_object(data, raw_entity_mappings)
163
+ misconfigurations: dict[str, str] = {}
164
+ mapped_entity = await self._search_as_object(
165
+ data, raw_entity_mappings, misconfigurations
166
+ )
136
167
  return MappedEntity(
137
168
  mapped_entity,
138
169
  did_entity_pass_selector=should_run,
139
170
  raw_data=data if should_run else None,
171
+ misconfigurations=misconfigurations,
140
172
  )
141
173
 
142
174
  return MappedEntity()
@@ -218,7 +250,11 @@ class JQEntityProcessor(BaseEntityProcessor):
218
250
  passed_entities = []
219
251
  failed_entities = []
220
252
  examples_to_send: list[dict[str, Any]] = []
253
+ entity_misconfigurations: dict[str, str] = {}
254
+ missing_required_fields: bool = False
221
255
  for result in calculated_entities_results:
256
+ if len(result.misconfigurations) > 0:
257
+ entity_misconfigurations |= result.misconfigurations
222
258
  if result.entity.get("identifier") and result.entity.get("blueprint"):
223
259
  parsed_entity = Entity.parse_obj(result.entity)
224
260
  if result.did_entity_pass_selector:
@@ -230,6 +266,12 @@ class JQEntityProcessor(BaseEntityProcessor):
230
266
  examples_to_send.append(result.raw_data)
231
267
  else:
232
268
  failed_entities.append(parsed_entity)
269
+ else:
270
+ missing_required_fields = True
271
+ if len(entity_misconfigurations) > 0:
272
+ logger.info(
273
+ f"The mapping resulted with invalid values for{" identifier, blueprint," if missing_required_fields else " "} properties. Mapping result: {entity_misconfigurations}"
274
+ )
233
275
  if (
234
276
  not calculated_entities_results
235
277
  and raw_results
@@ -245,4 +287,5 @@ class JQEntityProcessor(BaseEntityProcessor):
245
287
  return CalculationResult(
246
288
  EntitySelectorDiff(passed=passed_entities, failed=failed_entities),
247
289
  errors,
290
+ misonfigured_entity_keys=entity_misconfigurations,
248
291
  )
@@ -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
@@ -184,7 +185,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
184
185
  send_raw_data_examples_amount = (
185
186
  SEND_RAW_DATA_EXAMPLES_AMOUNT if ocean.config.send_raw_data_examples else 0
186
187
  )
187
- all_entities, register_errors = await self._register_resource_raw(
188
+ all_entities, register_errors,_ = await self._register_resource_raw(
188
189
  resource_config,
189
190
  raw_results,
190
191
  user_agent_type,
@@ -201,7 +202,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
201
202
  0, send_raw_data_examples_amount - len(passed_entities)
202
203
  )
203
204
 
204
- entities, register_errors = await self._register_resource_raw(
205
+ entities, register_errors,_ = await self._register_resource_raw(
205
206
  resource_config,
206
207
  items,
207
208
  user_agent_type,
@@ -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
@@ -1,5 +1,13 @@
1
- from typing import TypedDict, Any, AsyncIterator, Callable, Awaitable, NamedTuple
2
-
1
+ from typing import (
2
+ TypedDict,
3
+ Any,
4
+ AsyncIterator,
5
+ Callable,
6
+ Awaitable,
7
+ NamedTuple,
8
+ )
9
+
10
+ from dataclasses import field
3
11
  from port_ocean.core.models import Entity
4
12
 
5
13
  RAW_ITEM = dict[Any, Any]
@@ -30,6 +38,7 @@ class EntitySelectorDiff(NamedTuple):
30
38
  class CalculationResult(NamedTuple):
31
39
  entity_selector_diff: EntitySelectorDiff
32
40
  errors: list[Exception]
41
+ misonfigured_entity_keys: dict[str, str] = field(default_factory=dict)
33
42
 
34
43
 
35
44
  class IntegrationEventsCallbacks(TypedDict):
@@ -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
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
@@ -269,3 +269,37 @@ class TestJQEntityProcessor:
269
269
  assert len(result.entity_selector_diff.passed) == 1
270
270
  assert result.entity_selector_diff.passed[0].properties.get("foo") == "bar"
271
271
  assert not result.errors
272
+
273
+ async def test_parse_items_wrong_mapping(
274
+ self, mocked_processor: JQEntityProcessor
275
+ ) -> None:
276
+ mapping = Mock()
277
+ mapping.port.entity.mappings.dict.return_value = {
278
+ "title": ".foo",
279
+ "identifier": ".ark",
280
+ "blueprint": ".baz",
281
+ "properties": {
282
+ "description": ".bazbar",
283
+ "url": ".foobar",
284
+ "defaultBranch": ".bar.baz",
285
+ },
286
+ }
287
+ mapping.port.items_to_parse = None
288
+ mapping.selector.query = "true"
289
+ raw_results = [
290
+ {
291
+ "foo": "bar",
292
+ "baz": "bazbar",
293
+ "bar": {"foobar": "barfoo", "baz": "barbaz"},
294
+ },
295
+ {"foo": "bar", "baz": "bazbar", "bar": {"foobar": "foobar"}},
296
+ ]
297
+ result = await mocked_processor._parse_items(mapping, raw_results)
298
+ assert len(result.misonfigured_entity_keys) > 0
299
+ assert len(result.misonfigured_entity_keys) == 4
300
+ assert result.misonfigured_entity_keys == {
301
+ "identifier": ".ark",
302
+ "description": ".bazbar",
303
+ "url": ".foobar",
304
+ "defaultBranch": ".bar.baz",
305
+ }
@@ -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.3
3
+ Version: 0.16.1
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
@@ -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=X-up0HVdE8pkITxzvB1BC7W8Oq0C14WbT3WqV7p-wJc,11129
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,12 @@ 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
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
103
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=Wir4aTSCkIvG6Ny9Eo0Xf55OkSbh_6wHfNSaCffAKJQ,20279
104
+ port_ocean/core/integrations/mixins/utils.py,sha256=oN4Okz6xlaefpid1_Pud8HPSw9BwwjRohyNsknq-Myg,2309
105
+ port_ocean/core/models.py,sha256=O8nOKc4ORZz9tS5s6y5YgGLEBroXpvSPDqKuz48uKvs,1965
106
+ port_ocean/core/ocean_types.py,sha256=j_-or1VxDy22whLLxwxgzIsE4wAhFLH19Xff9l4oJA8,1124
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
108
109
  port_ocean/debug_cli.py,sha256=gHrv-Ey3cImKOcGZpjoHlo4pa_zfmyOl6TUM4o9VtcA,96
109
110
  port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
111
  port_ocean/exceptions/api.py,sha256=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
@@ -124,14 +125,16 @@ port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2
124
125
  port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
125
126
  port_ocean/ocean.py,sha256=XxO-aRExs1hcy6aJY_nceu-QXRWB2ZLpkIPPuBkp-bQ,5247
126
127
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
- port_ocean/run.py,sha256=rTxBlrQd4yyrtgErCFJCHCEHs7d1OXrRiJehUYmIbN0,2212
128
+ port_ocean/run.py,sha256=YnqchtVI6diuc6HcyqjcKVqcO0PYnfYjVKVcwKeNO2E,2218
128
129
  port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
129
130
  port_ocean/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
130
131
  port_ocean/tests/clients/port/mixins/test_entities.py,sha256=A9myrnkLhKSQrnOLv1Zz2wiOVSxW65Q9RIUIRbn_V7w,1586
131
132
  port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
132
133
  port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
133
- port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=Yv03P-LDcJCKZ21exiTFrcT1eu0zn6Z954dilxrb52Y,10842
134
- port_ocean/tests/core/test_utils.py,sha256=94940TerN38jy81ebLJ2Fzf5JJUaV9krnce75APCwmM,2641
134
+ port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=C2nLgapTdXRrzP5B4xBuHcc14L-NztFpxLIv8Iuv6Gg,12046
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
135
138
  port_ocean/tests/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
139
  port_ocean/tests/helpers/fake_port_api.py,sha256=9rtjC6iTQMfzWK6WipkDzzG0b1IIaRmvdJLOyV613vE,6479
137
140
  port_ocean/tests/helpers/fixtures.py,sha256=IQEplbHhRgjrAsZlnXrgSYA5YQEn25I9HgO3_Fjibxg,1481
@@ -153,8 +156,8 @@ port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,32
153
156
  port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
154
157
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
155
158
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
156
- port_ocean-0.15.3.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
157
- port_ocean-0.15.3.dist-info/METADATA,sha256=97A1rlQ3w2zUQl6-OKN_6n_yJvMquXcJWwsJ3bNJ3hg,6673
158
- port_ocean-0.15.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
159
- port_ocean-0.15.3.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
160
- port_ocean-0.15.3.dist-info/RECORD,,
159
+ port_ocean-0.16.1.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
160
+ port_ocean-0.16.1.dist-info/METADATA,sha256=6Zbwkb10_nBvr-Yan20BoYf42oD77C0je7Ft2UXmZHs,6673
161
+ port_ocean-0.16.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
162
+ port_ocean-0.16.1.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
163
+ port_ocean-0.16.1.dist-info/RECORD,,
File without changes