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.
- port_ocean/clients/port/mixins/entities.py +33 -4
- port_ocean/context/event.py +11 -0
- port_ocean/core/defaults/initialize.py +1 -1
- port_ocean/core/handlers/entities_state_applier/port/applier.py +9 -17
- port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +5 -2
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py +51 -8
- port_ocean/core/integrations/mixins/sync_raw.py +21 -4
- port_ocean/core/integrations/mixins/utils.py +1 -1
- port_ocean/core/models.py +4 -0
- port_ocean/core/ocean_types.py +11 -2
- port_ocean/core/utils/entity_topological_sorter.py +90 -0
- port_ocean/run.py +1 -1
- port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +34 -0
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +400 -0
- port_ocean/tests/core/test_utils.py +1 -1
- port_ocean/tests/core/utils/test_entity_topological_sorter.py +99 -0
- {port_ocean-0.15.3.dist-info → port_ocean-0.16.1.dist-info}/METADATA +1 -1
- {port_ocean-0.15.3.dist-info → port_ocean-0.16.1.dist-info}/RECORD +22 -19
- /port_ocean/core/{utils.py → utils/utils.py} +0 -0
- {port_ocean-0.15.3.dist-info → port_ocean-0.16.1.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.15.3.dist-info → port_ocean-0.16.1.dist-info}/WHEEL +0 -0
- {port_ocean-0.15.3.dist-info → port_ocean-0.16.1.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
port_ocean/context/event.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
port_ocean/core/ocean_types.py
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
from typing import
|
|
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
|
+
)
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
104
|
-
port_ocean/core/integrations/mixins/utils.py,sha256=
|
|
105
|
-
port_ocean/core/models.py,sha256=
|
|
106
|
-
port_ocean/core/ocean_types.py,sha256=
|
|
107
|
-
port_ocean/core/utils.py,sha256=
|
|
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=
|
|
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=
|
|
134
|
-
port_ocean/tests/core/
|
|
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.
|
|
157
|
-
port_ocean-0.
|
|
158
|
-
port_ocean-0.
|
|
159
|
-
port_ocean-0.
|
|
160
|
-
port_ocean-0.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|