port-ocean 0.5.5__py3-none-any.whl → 0.17.8__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.
- integrations/_infra/Dockerfile.Deb +56 -0
- integrations/_infra/Dockerfile.alpine +108 -0
- integrations/_infra/Dockerfile.base.builder +26 -0
- integrations/_infra/Dockerfile.base.runner +13 -0
- integrations/_infra/Dockerfile.dockerignore +94 -0
- {port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}} → integrations/_infra}/Makefile +21 -8
- integrations/_infra/grpcio.sh +18 -0
- integrations/_infra/init.sh +5 -0
- port_ocean/bootstrap.py +1 -1
- port_ocean/cli/commands/defaults/clean.py +3 -1
- port_ocean/cli/commands/new.py +42 -7
- port_ocean/cli/commands/sail.py +7 -1
- port_ocean/cli/cookiecutter/cookiecutter.json +3 -0
- port_ocean/cli/cookiecutter/hooks/post_gen_project.py +20 -3
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +6 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +41 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +16 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +6 -7
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +1 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +7 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +1 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +16 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +21 -10
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +2 -0
- port_ocean/clients/port/authentication.py +16 -4
- port_ocean/clients/port/client.py +17 -0
- port_ocean/clients/port/mixins/blueprints.py +7 -8
- port_ocean/clients/port/mixins/entities.py +108 -53
- port_ocean/clients/port/mixins/integrations.py +23 -34
- port_ocean/clients/port/retry_transport.py +0 -5
- port_ocean/clients/port/utils.py +9 -3
- port_ocean/config/base.py +16 -16
- port_ocean/config/dynamic.py +2 -0
- port_ocean/config/settings.py +79 -11
- port_ocean/context/event.py +18 -5
- port_ocean/context/ocean.py +14 -3
- port_ocean/core/defaults/clean.py +10 -3
- port_ocean/core/defaults/common.py +25 -9
- port_ocean/core/defaults/initialize.py +111 -100
- port_ocean/core/event_listener/__init__.py +8 -0
- port_ocean/core/event_listener/base.py +49 -10
- port_ocean/core/event_listener/factory.py +9 -1
- port_ocean/core/event_listener/http.py +11 -3
- port_ocean/core/event_listener/kafka.py +24 -5
- port_ocean/core/event_listener/once.py +96 -4
- port_ocean/core/event_listener/polling.py +16 -14
- port_ocean/core/event_listener/webhooks_only.py +41 -0
- port_ocean/core/handlers/__init__.py +1 -2
- port_ocean/core/handlers/entities_state_applier/base.py +4 -1
- port_ocean/core/handlers/entities_state_applier/port/applier.py +29 -87
- port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +5 -2
- port_ocean/core/handlers/entity_processor/base.py +26 -22
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py +253 -45
- port_ocean/core/handlers/port_app_config/base.py +55 -15
- port_ocean/core/handlers/port_app_config/models.py +24 -5
- port_ocean/core/handlers/resync_state_updater/__init__.py +5 -0
- port_ocean/core/handlers/resync_state_updater/updater.py +84 -0
- port_ocean/core/integrations/base.py +5 -7
- port_ocean/core/integrations/mixins/events.py +3 -1
- port_ocean/core/integrations/mixins/sync.py +4 -2
- port_ocean/core/integrations/mixins/sync_raw.py +209 -74
- port_ocean/core/integrations/mixins/utils.py +1 -1
- port_ocean/core/models.py +44 -0
- port_ocean/core/ocean_types.py +29 -11
- port_ocean/core/utils/entity_topological_sorter.py +90 -0
- port_ocean/core/utils/utils.py +109 -0
- port_ocean/debug_cli.py +5 -0
- port_ocean/exceptions/core.py +4 -0
- port_ocean/exceptions/port_defaults.py +0 -2
- port_ocean/helpers/retry.py +85 -24
- port_ocean/log/handlers.py +23 -2
- port_ocean/log/logger_setup.py +8 -1
- port_ocean/log/sensetive.py +25 -10
- port_ocean/middlewares.py +10 -2
- port_ocean/ocean.py +57 -24
- port_ocean/run.py +10 -5
- port_ocean/tests/__init__.py +0 -0
- port_ocean/tests/clients/port/mixins/test_entities.py +53 -0
- port_ocean/tests/conftest.py +4 -0
- port_ocean/tests/core/defaults/test_common.py +166 -0
- port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +350 -0
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +552 -0
- port_ocean/tests/core/test_utils.py +73 -0
- port_ocean/tests/core/utils/test_entity_topological_sorter.py +99 -0
- port_ocean/tests/helpers/__init__.py +0 -0
- port_ocean/tests/helpers/fake_port_api.py +191 -0
- port_ocean/tests/helpers/fixtures.py +46 -0
- port_ocean/tests/helpers/integration.py +31 -0
- port_ocean/tests/helpers/ocean_app.py +66 -0
- port_ocean/tests/helpers/port_client.py +21 -0
- port_ocean/tests/helpers/smoke_test.py +82 -0
- port_ocean/tests/log/test_handlers.py +71 -0
- port_ocean/tests/test_smoke.py +74 -0
- port_ocean/tests/utils/test_async_iterators.py +45 -0
- port_ocean/tests/utils/test_cache.py +189 -0
- port_ocean/utils/async_iterators.py +109 -0
- port_ocean/utils/cache.py +37 -1
- port_ocean/utils/misc.py +22 -4
- port_ocean/utils/queue_utils.py +88 -0
- port_ocean/utils/signal.py +1 -4
- port_ocean/utils/time.py +54 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/METADATA +27 -19
- port_ocean-0.17.8.dist-info/RECORD +164 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/WHEEL +1 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -94
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -15
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -17
- port_ocean/core/handlers/entities_state_applier/port/validate_entity_relations.py +0 -40
- port_ocean/core/utils.py +0 -65
- port_ocean-0.5.5.dist-info/RECORD +0 -129
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/entry_points.txt +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
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
|
|
5
6
|
|
|
7
|
+
import httpx
|
|
6
8
|
from loguru import logger
|
|
7
9
|
|
|
8
10
|
from port_ocean.clients.port.types import UserAgentType
|
|
@@ -22,12 +24,15 @@ from port_ocean.core.ocean_types import (
|
|
|
22
24
|
RAW_RESULT,
|
|
23
25
|
RESYNC_RESULT,
|
|
24
26
|
RawEntityDiff,
|
|
25
|
-
EntityDiff,
|
|
26
27
|
ASYNC_GENERATOR_RESYNC_TYPE,
|
|
28
|
+
RAW_ITEM,
|
|
29
|
+
CalculationResult,
|
|
27
30
|
)
|
|
28
|
-
from port_ocean.core.utils import zip_and_sum
|
|
31
|
+
from port_ocean.core.utils.utils import zip_and_sum, gather_and_split_errors_from_results
|
|
29
32
|
from port_ocean.exceptions.core import OceanAbortException
|
|
30
33
|
|
|
34
|
+
SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
33
38
|
"""Mixin class for synchronization of raw constructed entities.
|
|
@@ -86,7 +91,6 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
86
91
|
) -> tuple[RESYNC_RESULT, list[RAW_RESULT | Exception]]:
|
|
87
92
|
tasks = []
|
|
88
93
|
results = []
|
|
89
|
-
|
|
90
94
|
for task in fns:
|
|
91
95
|
if inspect.isasyncgenfunction(task):
|
|
92
96
|
results.append(resync_generator_wrapper(task, resource_config.kind))
|
|
@@ -97,21 +101,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
97
101
|
logger.info(
|
|
98
102
|
f"Found {len(tasks) + len(results)} resync tasks for {resource_config.kind}"
|
|
99
103
|
)
|
|
100
|
-
|
|
101
|
-
results_with_error = await asyncio.gather(*tasks, return_exceptions=True)
|
|
104
|
+
successful_results, errors = await gather_and_split_errors_from_results(tasks)
|
|
102
105
|
results.extend(
|
|
103
106
|
sum(
|
|
104
|
-
|
|
105
|
-
result
|
|
106
|
-
for result in results_with_error
|
|
107
|
-
if not isinstance(result, Exception)
|
|
108
|
-
],
|
|
107
|
+
successful_results,
|
|
109
108
|
[],
|
|
110
109
|
)
|
|
111
110
|
)
|
|
112
|
-
errors = [
|
|
113
|
-
result for result in results_with_error if isinstance(result, Exception)
|
|
114
|
-
]
|
|
115
111
|
|
|
116
112
|
logger.info(
|
|
117
113
|
f"Triggered {len(tasks)} tasks for {resource_config.kind}, failed: {len(errors)}"
|
|
@@ -120,12 +116,16 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
120
116
|
return results, errors
|
|
121
117
|
|
|
122
118
|
async def _calculate_raw(
|
|
123
|
-
self,
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
self,
|
|
120
|
+
raw_diff: list[tuple[ResourceConfig, list[RAW_ITEM]]],
|
|
121
|
+
parse_all: bool = False,
|
|
122
|
+
send_raw_data_examples_amount: int = 0,
|
|
123
|
+
) -> list[CalculationResult]:
|
|
126
124
|
return await asyncio.gather(
|
|
127
125
|
*(
|
|
128
|
-
self.entity_processor.parse_items(
|
|
126
|
+
self.entity_processor.parse_items(
|
|
127
|
+
mapping, results, parse_all, send_raw_data_examples_amount
|
|
128
|
+
)
|
|
129
129
|
for mapping, results in raw_diff
|
|
130
130
|
)
|
|
131
131
|
)
|
|
@@ -135,45 +135,41 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
135
135
|
resource: ResourceConfig,
|
|
136
136
|
results: list[dict[Any, Any]],
|
|
137
137
|
user_agent_type: UserAgentType,
|
|
138
|
-
|
|
138
|
+
parse_all: bool = False,
|
|
139
|
+
send_raw_data_examples_amount: int = 0,
|
|
140
|
+
) -> CalculationResult:
|
|
139
141
|
objects_diff = await self._calculate_raw(
|
|
140
|
-
[
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
]
|
|
142
|
+
[(resource, results)], parse_all, send_raw_data_examples_amount
|
|
143
|
+
)
|
|
144
|
+
modified_objects = await self.entities_state_applier.upsert(
|
|
145
|
+
objects_diff[0].entity_selector_diff.passed, user_agent_type
|
|
146
|
+
)
|
|
147
|
+
return CalculationResult(
|
|
148
|
+
objects_diff[0].entity_selector_diff._replace(passed=modified_objects),
|
|
149
|
+
errors=objects_diff[0].errors,
|
|
150
|
+
misonfigured_entity_keys=objects_diff[0].misonfigured_entity_keys,
|
|
149
151
|
)
|
|
150
|
-
|
|
151
|
-
entities_after: list[Entity] = objects_diff[0]["after"]
|
|
152
|
-
await self.entities_state_applier.upsert(entities_after, user_agent_type)
|
|
153
|
-
return entities_after
|
|
154
152
|
|
|
155
153
|
async def _unregister_resource_raw(
|
|
156
154
|
self,
|
|
157
155
|
resource: ResourceConfig,
|
|
158
|
-
results: list[
|
|
156
|
+
results: list[RAW_ITEM],
|
|
159
157
|
user_agent_type: UserAgentType,
|
|
160
|
-
) -> list[Entity]:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"before": results,
|
|
167
|
-
"after": [],
|
|
168
|
-
},
|
|
169
|
-
)
|
|
170
|
-
]
|
|
171
|
-
)
|
|
158
|
+
) -> tuple[list[Entity], list[Exception]]:
|
|
159
|
+
if resource.port.entity.mappings.is_using_search_identifier:
|
|
160
|
+
logger.info(
|
|
161
|
+
f"Skip unregistering resource of kind {resource.kind}, as mapping defined with search identifier"
|
|
162
|
+
)
|
|
163
|
+
return [], []
|
|
172
164
|
|
|
173
|
-
|
|
174
|
-
|
|
165
|
+
objects_diff = await self._calculate_raw([(resource, results)])
|
|
166
|
+
entities_selector_diff, errors, _ = objects_diff[0]
|
|
167
|
+
|
|
168
|
+
await self.entities_state_applier.delete(
|
|
169
|
+
entities_selector_diff.passed, user_agent_type
|
|
170
|
+
)
|
|
175
171
|
logger.info("Finished unregistering change")
|
|
176
|
-
return
|
|
172
|
+
return entities_selector_diff.passed, errors
|
|
177
173
|
|
|
178
174
|
async def _register_in_batches(
|
|
179
175
|
self, resource_config: ResourceConfig, user_agent_type: UserAgentType
|
|
@@ -187,25 +183,41 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
187
183
|
else:
|
|
188
184
|
async_generators.append(result)
|
|
189
185
|
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
send_raw_data_examples_amount = (
|
|
187
|
+
SEND_RAW_DATA_EXAMPLES_AMOUNT if ocean.config.send_raw_data_examples else 0
|
|
192
188
|
)
|
|
189
|
+
all_entities, register_errors,_ = await self._register_resource_raw(
|
|
190
|
+
resource_config,
|
|
191
|
+
raw_results,
|
|
192
|
+
user_agent_type,
|
|
193
|
+
send_raw_data_examples_amount=send_raw_data_examples_amount,
|
|
194
|
+
)
|
|
195
|
+
errors.extend(register_errors)
|
|
196
|
+
passed_entities = list(all_entities.passed)
|
|
193
197
|
|
|
194
198
|
for generator in async_generators:
|
|
195
199
|
try:
|
|
196
200
|
async for items in generator:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
if send_raw_data_examples_amount > 0:
|
|
202
|
+
send_raw_data_examples_amount = max(
|
|
203
|
+
0, send_raw_data_examples_amount - len(passed_entities)
|
|
200
204
|
)
|
|
205
|
+
|
|
206
|
+
entities, register_errors,_ = await self._register_resource_raw(
|
|
207
|
+
resource_config,
|
|
208
|
+
items,
|
|
209
|
+
user_agent_type,
|
|
210
|
+
send_raw_data_examples_amount=send_raw_data_examples_amount,
|
|
201
211
|
)
|
|
212
|
+
errors.extend(register_errors)
|
|
213
|
+
passed_entities.extend(entities.passed)
|
|
202
214
|
except* OceanAbortException as error:
|
|
203
215
|
errors.append(error)
|
|
204
216
|
|
|
205
217
|
logger.info(
|
|
206
|
-
f"Finished registering change for {len(results)} raw results for kind: {resource_config.kind}. {len(
|
|
218
|
+
f"Finished registering change for {len(results)} raw results for kind: {resource_config.kind}. {len(passed_entities)} entities were affected"
|
|
207
219
|
)
|
|
208
|
-
return
|
|
220
|
+
return passed_entities, errors
|
|
209
221
|
|
|
210
222
|
async def register_raw(
|
|
211
223
|
self,
|
|
@@ -231,13 +243,63 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
231
243
|
resource for resource in config.resources if resource.kind == kind
|
|
232
244
|
]
|
|
233
245
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
if not resource_mappings:
|
|
247
|
+
return []
|
|
248
|
+
|
|
249
|
+
diffs, errors, misconfigured_entity_keys = zip(
|
|
250
|
+
*await asyncio.gather(
|
|
251
|
+
*(
|
|
252
|
+
self._register_resource_raw(
|
|
253
|
+
resource, results, user_agent_type, True
|
|
254
|
+
)
|
|
255
|
+
for resource in resource_mappings
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
diffs = list(diffs)
|
|
261
|
+
errors = sum(errors, [])
|
|
262
|
+
misconfigured_entity_keys = list(misconfigured_entity_keys)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if errors:
|
|
266
|
+
message = f"Failed to register {len(errors)} entities. Skipping delete phase due to incomplete state"
|
|
267
|
+
logger.error(message, exc_info=errors)
|
|
268
|
+
raise ExceptionGroup(
|
|
269
|
+
message,
|
|
270
|
+
errors,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
registered_entities, entities_to_delete = zip_and_sum(diffs)
|
|
274
|
+
|
|
275
|
+
registered_entities_attributes = {
|
|
276
|
+
(entity.identifier, entity.blueprint) for entity in registered_entities
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
filtered_entities_to_delete: list[Entity] = (
|
|
280
|
+
await ocean.port_client.search_batch_entities(
|
|
281
|
+
user_agent_type,
|
|
282
|
+
[
|
|
283
|
+
entity
|
|
284
|
+
for entity in entities_to_delete
|
|
285
|
+
if not entity.is_using_search_identifier
|
|
286
|
+
and (entity.identifier, entity.blueprint)
|
|
287
|
+
not in registered_entities_attributes
|
|
288
|
+
],
|
|
238
289
|
)
|
|
239
290
|
)
|
|
240
291
|
|
|
292
|
+
if filtered_entities_to_delete:
|
|
293
|
+
logger.info(
|
|
294
|
+
f"Deleting {len(filtered_entities_to_delete)} entities that didn't pass any of the selectors"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
await self.entities_state_applier.delete(
|
|
298
|
+
filtered_entities_to_delete, user_agent_type
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return registered_entities
|
|
302
|
+
|
|
241
303
|
async def unregister_raw(
|
|
242
304
|
self,
|
|
243
305
|
kind: str,
|
|
@@ -262,13 +324,25 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
262
324
|
resource for resource in config.resources if resource.kind == kind
|
|
263
325
|
]
|
|
264
326
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
327
|
+
entities, errors = zip_and_sum(
|
|
328
|
+
await asyncio.gather(
|
|
329
|
+
*(
|
|
330
|
+
self._unregister_resource_raw(resource, results, user_agent_type)
|
|
331
|
+
for resource in resource_mappings
|
|
332
|
+
)
|
|
269
333
|
)
|
|
270
334
|
)
|
|
271
335
|
|
|
336
|
+
if errors:
|
|
337
|
+
message = f"Failed to unregister all entities with {len(errors)} errors"
|
|
338
|
+
logger.error(message, exc_info=errors)
|
|
339
|
+
raise ExceptionGroup(
|
|
340
|
+
message,
|
|
341
|
+
errors,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return entities
|
|
345
|
+
|
|
272
346
|
async def update_raw_diff(
|
|
273
347
|
self,
|
|
274
348
|
kind: str,
|
|
@@ -293,21 +367,53 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
293
367
|
with logger.contextualize(kind=kind):
|
|
294
368
|
logger.info(f"Found {len(resource_mappings)} resources for {kind}")
|
|
295
369
|
|
|
296
|
-
|
|
297
|
-
|
|
370
|
+
entities_before, _ = zip(
|
|
371
|
+
await self._calculate_raw(
|
|
372
|
+
[
|
|
373
|
+
(mapping, raw_desired_state["before"])
|
|
374
|
+
for mapping in resource_mappings
|
|
375
|
+
]
|
|
376
|
+
)
|
|
298
377
|
)
|
|
299
378
|
|
|
300
|
-
|
|
301
|
-
(
|
|
302
|
-
(entities_change["before"], entities_change["after"])
|
|
303
|
-
for entities_change in objects_diff
|
|
304
|
-
)
|
|
379
|
+
entities_after, after_errors = await self._calculate_raw(
|
|
380
|
+
[(mapping, raw_desired_state["after"]) for mapping in resource_mappings]
|
|
305
381
|
)
|
|
306
382
|
|
|
307
|
-
|
|
308
|
-
|
|
383
|
+
entities_before_flatten: list[Entity] = sum(
|
|
384
|
+
(entities_diff.passed for entities_diff in entities_before), []
|
|
309
385
|
)
|
|
310
386
|
|
|
387
|
+
entities_after_flatten: list[Entity] = sum(
|
|
388
|
+
(entities_diff.passed for entities_diff in entities_after), []
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if after_errors:
|
|
392
|
+
message = (
|
|
393
|
+
f"Failed to calculate diff for entities with {len(after_errors)} errors. "
|
|
394
|
+
f"Skipping delete phase due to incomplete state"
|
|
395
|
+
)
|
|
396
|
+
logger.error(message, exc_info=after_errors)
|
|
397
|
+
entities_before_flatten = []
|
|
398
|
+
|
|
399
|
+
await self.entities_state_applier.apply_diff(
|
|
400
|
+
{"before": entities_before_flatten, "after": entities_after_flatten},
|
|
401
|
+
user_agent_type,
|
|
402
|
+
)
|
|
403
|
+
async def sort_and_upsert_failed_entities(self,user_agent_type: UserAgentType)->None:
|
|
404
|
+
try:
|
|
405
|
+
if not event.entity_topological_sorter.should_execute():
|
|
406
|
+
return None
|
|
407
|
+
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())
|
|
408
|
+
|
|
409
|
+
for entity in event.entity_topological_sorter.get_entities():
|
|
410
|
+
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)
|
|
411
|
+
|
|
412
|
+
except OceanAbortException as ocean_abort:
|
|
413
|
+
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() )
|
|
414
|
+
if isinstance(ocean_abort.__cause__,CycleError):
|
|
415
|
+
for entity in event.entity_topological_sorter.get_entities(False):
|
|
416
|
+
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)
|
|
311
417
|
async def sync_raw_all(
|
|
312
418
|
self,
|
|
313
419
|
_: dict[Any, Any] | None = None,
|
|
@@ -331,9 +437,27 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
331
437
|
EventType.RESYNC,
|
|
332
438
|
trigger_type=trigger_type,
|
|
333
439
|
):
|
|
334
|
-
|
|
440
|
+
# If a resync is triggered due to a mappings change, we want to make sure that we have the updated version
|
|
441
|
+
# rather than the old cache
|
|
442
|
+
app_config = await self.port_app_config_handler.get_port_app_config(
|
|
443
|
+
use_cache=False
|
|
444
|
+
)
|
|
445
|
+
logger.info(f"Resync will use the following mappings: {app_config.dict()}")
|
|
335
446
|
|
|
336
|
-
|
|
447
|
+
try:
|
|
448
|
+
did_fetched_current_state = True
|
|
449
|
+
entities_at_port = await ocean.port_client.search_entities(
|
|
450
|
+
user_agent_type
|
|
451
|
+
)
|
|
452
|
+
except httpx.HTTPError as e:
|
|
453
|
+
logger.warning(
|
|
454
|
+
"Failed to fetch the current state of entities at Port. "
|
|
455
|
+
"Skipping delete phase due to unknown initial state. "
|
|
456
|
+
f"Error: {e}\n"
|
|
457
|
+
f"Response status code: {e.response.status_code if isinstance(e, httpx.HTTPStatusError) else None}\n"
|
|
458
|
+
f"Response content: {e.response.text if isinstance(e, httpx.HTTPStatusError) else None}\n"
|
|
459
|
+
)
|
|
460
|
+
did_fetched_current_state = False
|
|
337
461
|
|
|
338
462
|
creation_results: list[tuple[list[Entity], list[Exception]]] = []
|
|
339
463
|
|
|
@@ -349,9 +473,19 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
349
473
|
event.on_abort(lambda: task.cancel())
|
|
350
474
|
|
|
351
475
|
creation_results.append(await task)
|
|
476
|
+
|
|
477
|
+
await self.sort_and_upsert_failed_entities(user_agent_type)
|
|
352
478
|
except asyncio.CancelledError as e:
|
|
353
|
-
logger.warning("Resync aborted successfully")
|
|
479
|
+
logger.warning("Resync aborted successfully, skipping delete phase. This leads to an incomplete state")
|
|
480
|
+
raise
|
|
354
481
|
else:
|
|
482
|
+
if not did_fetched_current_state:
|
|
483
|
+
logger.warning(
|
|
484
|
+
"Due to an error before the resync, the previous state of entities at Port is unknown."
|
|
485
|
+
" Skipping delete phase due to unknown initial state."
|
|
486
|
+
)
|
|
487
|
+
return
|
|
488
|
+
|
|
355
489
|
logger.info("Starting resync diff calculation")
|
|
356
490
|
flat_created_entities, errors = zip_and_sum(creation_results) or [
|
|
357
491
|
[],
|
|
@@ -376,3 +510,4 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
376
510
|
{"before": entities_at_port, "after": flat_created_entities},
|
|
377
511
|
user_agent_type,
|
|
378
512
|
)
|
|
513
|
+
logger.info("Resync finished successfully")
|
|
@@ -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
|
@@ -1,9 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
1
3
|
from typing import Any
|
|
2
4
|
|
|
3
5
|
from pydantic import BaseModel
|
|
4
6
|
from pydantic.fields import Field
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
class Runtime(Enum):
|
|
10
|
+
Saas = "Saas"
|
|
11
|
+
OnPrem = "OnPrem"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def is_saas_runtime(self) -> bool:
|
|
15
|
+
return self in [Runtime.Saas]
|
|
16
|
+
|
|
17
|
+
def is_installation_type_compatible(self, installation_type: str) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Check if the installation type is compatible with the runtime
|
|
20
|
+
|
|
21
|
+
if the runtime is Saas, the installation type should start with Saas
|
|
22
|
+
else the installation type should be OnPrem
|
|
23
|
+
"""
|
|
24
|
+
return (
|
|
25
|
+
self.value == Runtime.Saas.value
|
|
26
|
+
and installation_type.startswith(self.value)
|
|
27
|
+
) or installation_type == self.value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PortAPIErrorMessage(Enum):
|
|
31
|
+
NOT_FOUND = "not_found"
|
|
32
|
+
|
|
33
|
+
|
|
7
34
|
class Entity(BaseModel):
|
|
8
35
|
identifier: Any
|
|
9
36
|
blueprint: Any
|
|
@@ -12,6 +39,10 @@ class Entity(BaseModel):
|
|
|
12
39
|
properties: dict[str, Any] = {}
|
|
13
40
|
relations: dict[str, Any] = {}
|
|
14
41
|
|
|
42
|
+
@property
|
|
43
|
+
def is_using_search_identifier(self) -> bool:
|
|
44
|
+
return isinstance(self.identifier, dict)
|
|
45
|
+
|
|
15
46
|
|
|
16
47
|
class BlueprintRelation(BaseModel):
|
|
17
48
|
many: bool
|
|
@@ -34,3 +65,16 @@ class Migration(BaseModel):
|
|
|
34
65
|
sourceBlueprint: str
|
|
35
66
|
mapping: dict[str, Any]
|
|
36
67
|
status: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class EntityPortDiff:
|
|
72
|
+
"""Represents the differences between entities for porting.
|
|
73
|
+
|
|
74
|
+
This class holds the lists of deleted, modified, and created entities as part
|
|
75
|
+
of the porting process.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
deleted: list[Entity] = field(default_factory=list)
|
|
79
|
+
modified: list[Entity] = field(default_factory=list)
|
|
80
|
+
created: list[Entity] = field(default_factory=list)
|
port_ocean/core/ocean_types.py
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
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
|
|
|
13
|
+
RAW_ITEM = dict[Any, Any]
|
|
14
|
+
RAW_RESULT = list[RAW_ITEM]
|
|
15
|
+
ASYNC_GENERATOR_RESYNC_TYPE = AsyncIterator[RAW_RESULT]
|
|
16
|
+
RESYNC_RESULT = list[RAW_ITEM | ASYNC_GENERATOR_RESYNC_TYPE]
|
|
17
|
+
|
|
18
|
+
LISTENER_RESULT = Awaitable[RAW_RESULT] | ASYNC_GENERATOR_RESYNC_TYPE
|
|
19
|
+
RESYNC_EVENT_LISTENER = Callable[[str], LISTENER_RESULT]
|
|
20
|
+
START_EVENT_LISTENER = Callable[[], Awaitable[None]]
|
|
21
|
+
|
|
5
22
|
|
|
6
23
|
class RawEntityDiff(TypedDict):
|
|
7
|
-
before: list[
|
|
8
|
-
after: list[
|
|
24
|
+
before: list[RAW_ITEM]
|
|
25
|
+
after: list[RAW_ITEM]
|
|
9
26
|
|
|
10
27
|
|
|
11
28
|
class EntityDiff(TypedDict):
|
|
@@ -13,14 +30,15 @@ class EntityDiff(TypedDict):
|
|
|
13
30
|
after: list[Entity]
|
|
14
31
|
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
RESYNC_RESULT = list[RAW_ITEM | ASYNC_GENERATOR_RESYNC_TYPE]
|
|
33
|
+
class EntitySelectorDiff(NamedTuple):
|
|
34
|
+
passed: list[Entity]
|
|
35
|
+
failed: list[Entity]
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
|
|
38
|
+
class CalculationResult(NamedTuple):
|
|
39
|
+
entity_selector_diff: EntitySelectorDiff
|
|
40
|
+
errors: list[Exception]
|
|
41
|
+
misonfigured_entity_keys: dict[str, str] = field(default_factory=dict)
|
|
24
42
|
|
|
25
43
|
|
|
26
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
|