port-ocean 0.5.22__py3-none-any.whl → 0.5.24__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,4 +1,4 @@
1
- FROM python:3.11-slim-buster
1
+ FROM python:3.11-slim-bookworm
2
2
 
3
3
  ENV LIBRDKAFKA_VERSION 1.9.2
4
4
 
@@ -32,7 +32,7 @@ class EntityClientMixin:
32
32
  ) -> None:
33
33
  validation_only = request_options["validation_only"]
34
34
  async with self.semaphore:
35
- logger.info(
35
+ logger.debug(
36
36
  f"{'Validating' if validation_only else 'Upserting'} entity: {entity.identifier} of blueprint: {entity.blueprint}"
37
37
  )
38
38
  headers = await self.auth.headers(user_agent_type)
@@ -2,8 +2,8 @@ import asyncio
2
2
  from typing import Type, Any
3
3
 
4
4
  import httpx
5
- from starlette import status
6
5
  from loguru import logger
6
+ from starlette import status
7
7
 
8
8
  from port_ocean.clients.port.client import PortClient
9
9
  from port_ocean.clients.port.types import UserAgentType
@@ -12,6 +12,7 @@ from port_ocean.context.ocean import ocean
12
12
  from port_ocean.core.defaults.common import Defaults, get_port_integration_defaults
13
13
  from port_ocean.core.handlers.port_app_config.models import PortAppConfig
14
14
  from port_ocean.core.models import Blueprint
15
+ from port_ocean.core.utils import gather_and_split_errors_from_results
15
16
  from port_ocean.exceptions.port_defaults import (
16
17
  AbortDefaultCreationError,
17
18
  )
@@ -65,43 +66,30 @@ async def _create_resources(
65
66
  defaults.blueprints
66
67
  )
67
68
 
68
- blueprints_results = await asyncio.gather(
69
- *(
69
+ blueprints_results, _ = await gather_and_split_errors_from_results(
70
+ [
70
71
  port_client.get_blueprint(blueprint["identifier"], should_log=False)
71
72
  for blueprint in creation_stage
72
- ),
73
- return_exceptions=True,
73
+ ],
74
+ lambda item: isinstance(item, Blueprint),
74
75
  )
75
76
 
76
- existing_blueprints = [
77
- result.identifier
78
- for result in blueprints_results
79
- if not isinstance(result, httpx.HTTPStatusError)
80
- and isinstance(result, Blueprint)
81
- ]
82
-
83
- if existing_blueprints:
77
+ if blueprints_results:
84
78
  logger.info(
85
- f"Blueprints already exist: {existing_blueprints}. Skipping integration default creation..."
79
+ f"Blueprints already exist: {[result.identifier for result in blueprints_results]}. Skipping integration default creation..."
86
80
  )
87
81
  return
88
82
 
89
- create_results = await asyncio.gather(
90
- *(
83
+ created_blueprints, errors = await gather_and_split_errors_from_results(
84
+ (
91
85
  port_client.create_blueprint(
92
86
  blueprint, user_agent_type=UserAgentType.exporter
93
87
  )
94
88
  for blueprint in creation_stage
95
- ),
96
- return_exceptions=True,
89
+ )
97
90
  )
98
91
 
99
- errors = [result for result in create_results if isinstance(result, Exception)]
100
- created_blueprints = [
101
- result["identifier"]
102
- for result in create_results
103
- if not isinstance(result, BaseException)
104
- ]
92
+ created_blueprints_identifiers = [bp["identifier"] for bp in created_blueprints]
105
93
 
106
94
  if errors:
107
95
  for error in errors:
@@ -110,8 +98,8 @@ async def _create_resources(
110
98
  f"Failed to create resources: {error.response.text}. Rolling back changes..."
111
99
  )
112
100
 
113
- raise AbortDefaultCreationError(created_blueprints, errors)
114
- created_pages = []
101
+ raise AbortDefaultCreationError(created_blueprints_identifiers, errors)
102
+ created_pages_identifiers = []
115
103
  try:
116
104
  for patch_stage in blueprint_patches:
117
105
  await asyncio.gather(
@@ -141,19 +129,11 @@ async def _create_resources(
141
129
  )
142
130
  )
143
131
 
144
- create_pages_result = await asyncio.gather(
145
- *(port_client.create_page(page) for page in defaults.pages),
146
- return_exceptions=True,
132
+ created_pages, pages_errors = await gather_and_split_errors_from_results(
133
+ (port_client.create_page(page) for page in defaults.pages)
147
134
  )
148
-
149
- created_pages = [
150
- result.get("identifier", "")
151
- for result in create_pages_result
152
- if not isinstance(result, BaseException)
153
- ]
154
-
155
- pages_errors = [
156
- result for result in create_pages_result if isinstance(result, Exception)
135
+ created_pages_identifiers = [
136
+ page.get("identifier", "") for page in created_pages
157
137
  ]
158
138
 
159
139
  if pages_errors:
@@ -164,7 +144,9 @@ async def _create_resources(
164
144
  )
165
145
 
166
146
  raise AbortDefaultCreationError(
167
- created_blueprints, pages_errors, created_pages
147
+ created_blueprints_identifiers,
148
+ pages_errors,
149
+ created_pages_identifiers,
168
150
  )
169
151
 
170
152
  await port_client.create_integration(
@@ -176,7 +158,9 @@ async def _create_resources(
176
158
  logger.error(
177
159
  f"Failed to create resources: {err.response.text}. Rolling back changes..."
178
160
  )
179
- raise AbortDefaultCreationError(created_blueprints, [err], created_pages)
161
+ raise AbortDefaultCreationError(
162
+ created_blueprints_identifiers, [err], created_pages_identifiers
163
+ )
180
164
 
181
165
 
182
166
  async def _initialize_defaults(
@@ -4,7 +4,7 @@ from .entities_state_applier.base import (
4
4
  from .entities_state_applier.port.applier import (
5
5
  HttpEntitiesStateApplier,
6
6
  )
7
- from .entity_processor.base import BaseEntityProcessor, EntityPortDiff
7
+ from .entity_processor.base import BaseEntityProcessor
8
8
  from .entity_processor.jq_entity_processor import (
9
9
  JQEntityProcessor,
10
10
  )
@@ -12,7 +12,6 @@ from .port_app_config.api import APIPortAppConfig
12
12
  from .port_app_config.base import BasePortAppConfig
13
13
 
14
14
  __all__ = [
15
- "EntityPortDiff",
16
15
  "BaseEntityProcessor",
17
16
  "JQEntityProcessor",
18
17
  "BasePortAppConfig",
@@ -1,30 +1,16 @@
1
1
  from abc import abstractmethod
2
- from dataclasses import dataclass, field
3
2
 
4
3
  from loguru import logger
5
4
 
6
5
  from port_ocean.core.handlers.base import BaseHandler
7
6
  from port_ocean.core.handlers.port_app_config.models import ResourceConfig
8
- from port_ocean.core.models import Entity
9
7
  from port_ocean.core.ocean_types import (
10
8
  RAW_ITEM,
9
+ CalculationResult,
11
10
  EntitySelectorDiff,
12
11
  )
13
12
 
14
13
 
15
- @dataclass
16
- class EntityPortDiff:
17
- """Represents the differences between entities for porting.
18
-
19
- This class holds the lists of deleted, modified, and created entities as part
20
- of the porting process.
21
- """
22
-
23
- deleted: list[Entity] = field(default_factory=list)
24
- modified: list[Entity] = field(default_factory=list)
25
- created: list[Entity] = field(default_factory=list)
26
-
27
-
28
14
  class BaseEntityProcessor(BaseHandler):
29
15
  """Abstract base class for processing and parsing entities.
30
16
 
@@ -41,7 +27,7 @@ class BaseEntityProcessor(BaseHandler):
41
27
  raw_data: list[RAW_ITEM],
42
28
  parse_all: bool = False,
43
29
  send_raw_data_examples_amount: int = 0,
44
- ) -> EntitySelectorDiff:
30
+ ) -> CalculationResult:
45
31
  pass
46
32
 
47
33
  async def parse_items(
@@ -50,7 +36,7 @@ class BaseEntityProcessor(BaseHandler):
50
36
  raw_data: list[RAW_ITEM],
51
37
  parse_all: bool = False,
52
38
  send_raw_data_examples_amount: int = 0,
53
- ) -> EntitySelectorDiff:
39
+ ) -> CalculationResult:
54
40
  """Public method to parse raw entity data and map it to an EntityDiff.
55
41
 
56
42
  Args:
@@ -63,6 +49,9 @@ class BaseEntityProcessor(BaseHandler):
63
49
  EntityDiff: The parsed entity differences.
64
50
  """
65
51
  with logger.contextualize(kind=mapping.kind):
52
+ if not raw_data:
53
+ return CalculationResult(EntitySelectorDiff([], []), [])
54
+
66
55
  return await self._parse_items(
67
56
  mapping, raw_data, parse_all, send_raw_data_examples_amount
68
57
  )
@@ -14,7 +14,9 @@ from port_ocean.core.models import Entity
14
14
  from port_ocean.core.ocean_types import (
15
15
  RAW_ITEM,
16
16
  EntitySelectorDiff,
17
+ CalculationResult,
17
18
  )
19
+ from port_ocean.core.utils import gather_and_split_errors_from_results, zip_and_sum
18
20
  from port_ocean.exceptions.core import EntityProcessorException
19
21
  from port_ocean.utils.queue_utils import process_in_queue
20
22
 
@@ -111,32 +113,34 @@ class JQEntityProcessor(BaseEntityProcessor):
111
113
  items_to_parse: str | None,
112
114
  selector_query: str,
113
115
  parse_all: bool = False,
114
- ) -> list[MappedEntity]:
116
+ ) -> tuple[list[MappedEntity], list[Exception]]:
117
+ raw_data = [data.copy()]
115
118
  if items_to_parse:
116
119
  items = await self._search(data, items_to_parse)
117
- if isinstance(items, list):
118
- return await asyncio.gather(
119
- *[
120
- self._get_mapped_entity(
121
- {"item": item, **data},
122
- raw_entity_mappings,
123
- selector_query,
124
- parse_all,
125
- )
126
- for item in items
127
- ]
120
+ if not isinstance(items, list):
121
+ logger.warning(
122
+ f"Failed to parse items for JQ expression {items_to_parse}, Expected list but got {type(items)}."
123
+ f" Skipping..."
128
124
  )
129
- logger.warning(
130
- f"Failed to parse items for JQ expression {items_to_parse}, Expected list but got {type(items)}."
131
- f" Skipping..."
132
- )
133
- else:
134
- return [
135
- await self._get_mapped_entity(
136
- data, raw_entity_mappings, selector_query, parse_all
125
+ return [], []
126
+ raw_data = [{"item": item, **data} for item in items]
127
+
128
+ entities, errors = await gather_and_split_errors_from_results(
129
+ [
130
+ self._get_mapped_entity(
131
+ raw,
132
+ raw_entity_mappings,
133
+ selector_query,
134
+ parse_all,
137
135
  )
136
+ for raw in raw_data
138
137
  ]
139
- return [MappedEntity()]
138
+ )
139
+ if errors:
140
+ logger.error(
141
+ f"Failed to calculate entities with {len(errors)} errors. errors: {errors}"
142
+ )
143
+ return entities, errors
140
144
 
141
145
  @staticmethod
142
146
  async def _send_examples(data: list[dict[str, Any]], kind: str) -> None:
@@ -157,37 +161,44 @@ class JQEntityProcessor(BaseEntityProcessor):
157
161
  raw_results: list[RAW_ITEM],
158
162
  parse_all: bool = False,
159
163
  send_raw_data_examples_amount: int = 0,
160
- ) -> EntitySelectorDiff:
164
+ ) -> CalculationResult:
161
165
  raw_entity_mappings: dict[str, Any] = mapping.port.entity.mappings.dict(
162
166
  exclude_unset=True
163
167
  )
164
-
165
- calculated_entities_results = await process_in_queue(
166
- raw_results,
167
- self._calculate_entity,
168
- raw_entity_mappings,
169
- mapping.port.items_to_parse,
170
- mapping.selector.query,
171
- parse_all,
168
+ logger.info(f"Parsing {len(raw_results)} raw results into entities")
169
+ calculated_entities_results, errors = zip_and_sum(
170
+ await process_in_queue(
171
+ raw_results,
172
+ self._calculate_entity,
173
+ raw_entity_mappings,
174
+ mapping.port.items_to_parse,
175
+ mapping.selector.query,
176
+ parse_all,
177
+ )
178
+ )
179
+ logger.debug(
180
+ f"Finished parsing raw results into entities with {len(errors)} errors. errors: {errors}"
172
181
  )
173
182
 
174
183
  passed_entities = []
175
184
  failed_entities = []
176
185
  examples_to_send: list[dict[str, Any]] = []
177
- for entities_results in calculated_entities_results:
178
- for result in entities_results:
179
- if result.entity.get("identifier") and result.entity.get("blueprint"):
180
- parsed_entity = Entity.parse_obj(result.entity)
181
- if result.did_entity_pass_selector:
182
- passed_entities.append(parsed_entity)
183
- if (
184
- len(examples_to_send) < send_raw_data_examples_amount
185
- and result.raw_data is not None
186
- ):
187
- examples_to_send.append(result.raw_data)
188
- else:
189
- failed_entities.append(parsed_entity)
186
+ for result in calculated_entities_results:
187
+ if result.entity.get("identifier") and result.entity.get("blueprint"):
188
+ parsed_entity = Entity.parse_obj(result.entity)
189
+ if result.did_entity_pass_selector:
190
+ passed_entities.append(parsed_entity)
191
+ if (
192
+ len(examples_to_send) < send_raw_data_examples_amount
193
+ and result.raw_data is not None
194
+ ):
195
+ examples_to_send.append(result.raw_data)
196
+ else:
197
+ failed_entities.append(parsed_entity)
190
198
 
191
199
  await self._send_examples(examples_to_send, mapping.kind)
192
200
 
193
- return EntitySelectorDiff(passed=passed_entities, failed=failed_entities)
201
+ return CalculationResult(
202
+ EntitySelectorDiff(passed=passed_entities, failed=failed_entities),
203
+ errors,
204
+ )
@@ -3,6 +3,7 @@ import inspect
3
3
  import typing
4
4
  from typing import Callable, Awaitable, Any
5
5
 
6
+ import httpx
6
7
  from loguru import logger
7
8
 
8
9
  from port_ocean.clients.port.types import UserAgentType
@@ -24,12 +25,11 @@ from port_ocean.core.ocean_types import (
24
25
  RawEntityDiff,
25
26
  ASYNC_GENERATOR_RESYNC_TYPE,
26
27
  RAW_ITEM,
27
- EntitySelectorDiff,
28
+ CalculationResult,
28
29
  )
29
- from port_ocean.core.utils import zip_and_sum
30
+ from port_ocean.core.utils import zip_and_sum, gather_and_split_errors_from_results
30
31
  from port_ocean.exceptions.core import OceanAbortException
31
32
 
32
-
33
33
  SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
34
34
 
35
35
 
@@ -101,21 +101,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
101
101
  logger.info(
102
102
  f"Found {len(tasks) + len(results)} resync tasks for {resource_config.kind}"
103
103
  )
104
-
105
- results_with_error = await asyncio.gather(*tasks, return_exceptions=True)
104
+ successful_results, errors = await gather_and_split_errors_from_results(tasks)
106
105
  results.extend(
107
106
  sum(
108
- [
109
- result
110
- for result in results_with_error
111
- if not isinstance(result, Exception)
112
- ],
107
+ successful_results,
113
108
  [],
114
109
  )
115
110
  )
116
- errors = [
117
- result for result in results_with_error if isinstance(result, Exception)
118
- ]
119
111
 
120
112
  logger.info(
121
113
  f"Triggered {len(tasks)} tasks for {resource_config.kind}, failed: {len(errors)}"
@@ -128,7 +120,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
128
120
  raw_diff: list[tuple[ResourceConfig, list[RAW_ITEM]]],
129
121
  parse_all: bool = False,
130
122
  send_raw_data_examples_amount: int = 0,
131
- ) -> list[EntitySelectorDiff]:
123
+ ) -> list[CalculationResult]:
132
124
  return await asyncio.gather(
133
125
  *(
134
126
  self.entity_processor.parse_items(
@@ -145,12 +137,12 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
145
137
  user_agent_type: UserAgentType,
146
138
  parse_all: bool = False,
147
139
  send_raw_data_examples_amount: int = 0,
148
- ) -> EntitySelectorDiff:
140
+ ) -> CalculationResult:
149
141
  objects_diff = await self._calculate_raw(
150
142
  [(resource, results)], parse_all, send_raw_data_examples_amount
151
143
  )
152
144
  await self.entities_state_applier.upsert(
153
- objects_diff[0].passed, user_agent_type
145
+ objects_diff[0].entity_selector_diff.passed, user_agent_type
154
146
  )
155
147
 
156
148
  return objects_diff[0]
@@ -160,13 +152,15 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
160
152
  resource: ResourceConfig,
161
153
  results: list[RAW_ITEM],
162
154
  user_agent_type: UserAgentType,
163
- ) -> list[Entity]:
155
+ ) -> tuple[list[Entity], list[Exception]]:
164
156
  objects_diff = await self._calculate_raw([(resource, results)])
157
+ entities_selector_diff, errors = objects_diff[0]
165
158
 
166
- entities_after: list[Entity] = objects_diff[0].passed
167
- await self.entities_state_applier.delete(entities_after, user_agent_type)
159
+ await self.entities_state_applier.delete(
160
+ entities_selector_diff.passed, user_agent_type
161
+ )
168
162
  logger.info("Finished unregistering change")
169
- return entities_after
163
+ return entities_selector_diff.passed, errors
170
164
 
171
165
  async def _register_in_batches(
172
166
  self, resource_config: ResourceConfig, user_agent_type: UserAgentType
@@ -183,39 +177,38 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
183
177
  send_raw_data_examples_amount = (
184
178
  SEND_RAW_DATA_EXAMPLES_AMOUNT if ocean.config.send_raw_data_examples else 0
185
179
  )
186
- entities = (
187
- await self._register_resource_raw(
188
- resource_config,
189
- raw_results,
190
- user_agent_type,
191
- send_raw_data_examples_amount=send_raw_data_examples_amount,
192
- )
193
- ).passed
180
+ all_entities, register_errors = await self._register_resource_raw(
181
+ resource_config,
182
+ raw_results,
183
+ user_agent_type,
184
+ send_raw_data_examples_amount=send_raw_data_examples_amount,
185
+ )
186
+ errors.extend(register_errors)
187
+ passed_entities = list(all_entities.passed)
194
188
 
195
189
  for generator in async_generators:
196
190
  try:
197
191
  async for items in generator:
198
192
  if send_raw_data_examples_amount > 0:
199
193
  send_raw_data_examples_amount = max(
200
- 0, send_raw_data_examples_amount - len(entities)
194
+ 0, send_raw_data_examples_amount - len(passed_entities)
201
195
  )
202
- entities.extend(
203
- (
204
- await self._register_resource_raw(
205
- resource_config,
206
- items,
207
- user_agent_type,
208
- send_raw_data_examples_amount=send_raw_data_examples_amount,
209
- )
210
- ).passed
196
+
197
+ entities, register_errors = await self._register_resource_raw(
198
+ resource_config,
199
+ items,
200
+ user_agent_type,
201
+ send_raw_data_examples_amount=send_raw_data_examples_amount,
211
202
  )
203
+ errors.extend(register_errors)
204
+ passed_entities.extend(entities.passed)
212
205
  except* OceanAbortException as error:
213
206
  errors.append(error)
214
207
 
215
208
  logger.info(
216
- f"Finished registering change for {len(results)} raw results for kind: {resource_config.kind}. {len(entities)} entities were affected"
209
+ f"Finished registering change for {len(results)} raw results for kind: {resource_config.kind}. {len(passed_entities)} entities were affected"
217
210
  )
218
- return entities, errors
211
+ return passed_entities, errors
219
212
 
220
213
  async def register_raw(
221
214
  self,
@@ -244,16 +237,26 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
244
237
  if not resource_mappings:
245
238
  return []
246
239
 
247
- diffs: list[EntitySelectorDiff] = await asyncio.gather(
248
- *(
249
- self._register_resource_raw(resource, results, user_agent_type, True)
250
- for resource in resource_mappings
240
+ diffs, errors = zip(
241
+ await asyncio.gather(
242
+ *(
243
+ self._register_resource_raw(
244
+ resource, results, user_agent_type, True
245
+ )
246
+ for resource in resource_mappings
247
+ )
251
248
  )
252
249
  )
253
250
 
254
- registered_entities, entities_to_delete = zip_and_sum(
255
- (entities_diff.passed, entities_diff.failed) for entities_diff in diffs
256
- )
251
+ if errors:
252
+ message = f"Failed to register {len(errors)} entities. Skipping delete phase due to incomplete state"
253
+ logger.error(message, exc_info=errors)
254
+ raise ExceptionGroup(
255
+ message,
256
+ errors,
257
+ )
258
+
259
+ registered_entities, entities_to_delete = zip_and_sum(diffs)
257
260
 
258
261
  registered_entities_attributes = {
259
262
  (entity.identifier, entity.blueprint) for entity in registered_entities
@@ -306,13 +309,23 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
306
309
  resource for resource in config.resources if resource.kind == kind
307
310
  ]
308
311
 
309
- return await asyncio.gather(
312
+ entities, errors = await asyncio.gather(
310
313
  *(
311
314
  self._unregister_resource_raw(resource, results, user_agent_type)
312
315
  for resource in resource_mappings
313
316
  )
314
317
  )
315
318
 
319
+ if errors:
320
+ message = f"Failed to unregister all entities with {len(errors)} errors"
321
+ logger.error(message, exc_info=errors)
322
+ raise ExceptionGroup(
323
+ message,
324
+ errors,
325
+ )
326
+
327
+ return entities
328
+
316
329
  async def update_raw_diff(
317
330
  self,
318
331
  kind: str,
@@ -337,14 +350,16 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
337
350
  with logger.contextualize(kind=kind):
338
351
  logger.info(f"Found {len(resource_mappings)} resources for {kind}")
339
352
 
340
- entities_before = await self._calculate_raw(
341
- [
342
- (mapping, raw_desired_state["before"])
343
- for mapping in resource_mappings
344
- ]
353
+ entities_before, _ = zip(
354
+ await self._calculate_raw(
355
+ [
356
+ (mapping, raw_desired_state["before"])
357
+ for mapping in resource_mappings
358
+ ]
359
+ )
345
360
  )
346
361
 
347
- entities_after = await self._calculate_raw(
362
+ entities_after, after_errors = await self._calculate_raw(
348
363
  [(mapping, raw_desired_state["after"]) for mapping in resource_mappings]
349
364
  )
350
365
 
@@ -356,6 +371,14 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
356
371
  (entities_diff.passed for entities_diff in entities_after), []
357
372
  )
358
373
 
374
+ if after_errors:
375
+ message = (
376
+ f"Failed to calculate diff for entities with {len(after_errors)} errors. "
377
+ f"Skipping delete phase due to incomplete state"
378
+ )
379
+ logger.error(message, exc_info=after_errors)
380
+ entities_before_flatten = []
381
+
359
382
  await self.entities_state_applier.apply_diff(
360
383
  {"before": entities_before_flatten, "after": entities_after_flatten},
361
384
  user_agent_type,
@@ -390,6 +413,20 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
390
413
  use_cache=False
391
414
  )
392
415
 
416
+ try:
417
+ entities_at_port = await ocean.port_client.search_entities(
418
+ user_agent_type
419
+ )
420
+ except httpx.HTTPError as e:
421
+ logger.warning(
422
+ "Failed to fetch the current state of entities at Port. "
423
+ "Skipping delete phase due to unknown initial state. "
424
+ f"Error: {e}\n"
425
+ f"Response status code: {e.response.status_code if isinstance(e, httpx.HTTPStatusError) else None}\n"
426
+ f"Response content: {e.response.text if isinstance(e, httpx.HTTPStatusError) else None}\n"
427
+ )
428
+ entities_at_port = []
429
+
393
430
  creation_results: list[tuple[list[Entity], list[Exception]]] = []
394
431
 
395
432
  try:
@@ -407,6 +444,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
407
444
  except asyncio.CancelledError as e:
408
445
  logger.warning("Resync aborted successfully")
409
446
  else:
447
+ if not entities_at_port:
448
+ logger.warning(
449
+ "Due to an error before the resync, the previous state of entities at Port is unknown."
450
+ " Skipping delete phase due to unknown initial state."
451
+ )
452
+ return
453
+
410
454
  logger.info("Starting resync diff calculation")
411
455
  flat_created_entities, errors = zip_and_sum(creation_results) or [
412
456
  [],
@@ -424,11 +468,8 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
424
468
 
425
469
  logger.error(message, exc_info=error_group)
426
470
  else:
427
- entities_at_port = await ocean.port_client.search_entities(
428
- user_agent_type
429
- )
430
471
  logger.info(
431
- f"Running resync diff calculation, number of entities found at Port: {len(entities_at_port)}, number of entities found during sync: {len(flat_created_entities)}"
472
+ f"Running resync diff calculation, number of entities at Port before resync: {len(entities_at_port)}, number of entities created during sync: {len(flat_created_entities)}"
432
473
  )
433
474
  await self.entities_state_applier.delete_diff(
434
475
  {"before": entities_at_port, "after": flat_created_entities},
port_ocean/core/models.py CHANGED
@@ -1,3 +1,4 @@
1
+ from dataclasses import dataclass, field
1
2
  from typing import Any
2
3
 
3
4
  from pydantic import BaseModel
@@ -34,3 +35,16 @@ class Migration(BaseModel):
34
35
  sourceBlueprint: str
35
36
  mapping: dict[str, Any]
36
37
  status: str
38
+
39
+
40
+ @dataclass
41
+ class EntityPortDiff:
42
+ """Represents the differences between entities for porting.
43
+
44
+ This class holds the lists of deleted, modified, and created entities as part
45
+ of the porting process.
46
+ """
47
+
48
+ deleted: list[Entity] = field(default_factory=list)
49
+ modified: list[Entity] = field(default_factory=list)
50
+ created: list[Entity] = field(default_factory=list)
@@ -2,7 +2,6 @@ from typing import TypedDict, Any, AsyncIterator, Callable, Awaitable, NamedTupl
2
2
 
3
3
  from port_ocean.core.models import Entity
4
4
 
5
-
6
5
  RAW_ITEM = dict[Any, Any]
7
6
  RAW_RESULT = list[RAW_ITEM]
8
7
  ASYNC_GENERATOR_RESYNC_TYPE = AsyncIterator[RAW_RESULT]
@@ -28,6 +27,11 @@ class EntitySelectorDiff(NamedTuple):
28
27
  failed: list[Entity]
29
28
 
30
29
 
30
+ class CalculationResult(NamedTuple):
31
+ entity_selector_diff: EntitySelectorDiff
32
+ errors: list[Exception]
33
+
34
+
31
35
  class IntegrationEventsCallbacks(TypedDict):
32
36
  start: list[START_EVENT_LISTENER]
33
37
  resync: dict[str | None, list[RESYNC_EVENT_LISTENER]]
port_ocean/core/utils.py CHANGED
@@ -1,9 +1,10 @@
1
- from typing import Iterable, Any, TypeVar
1
+ import asyncio
2
+ from typing import Iterable, Any, TypeVar, Callable, Awaitable
2
3
 
3
4
  from pydantic import parse_obj_as, ValidationError
4
5
 
5
- from port_ocean.core.handlers.entity_processor.base import EntityPortDiff
6
6
  from port_ocean.core.models import Entity
7
+ from port_ocean.core.models import EntityPortDiff
7
8
  from port_ocean.core.ocean_types import RAW_RESULT
8
9
  from port_ocean.exceptions.core import RawObjectValidationException
9
10
 
@@ -28,6 +29,31 @@ def is_same_entity(first_entity: Entity, second_entity: Entity) -> bool:
28
29
  )
29
30
 
30
31
 
32
+ Q = TypeVar("Q")
33
+
34
+
35
+ async def gather_and_split_errors_from_results(
36
+ task: Iterable[Awaitable[Q]],
37
+ result_threshold_validation: Callable[[Q | Exception], bool] | None = None,
38
+ ) -> tuple[list[Q], list[Exception]]:
39
+ valid_items: list[Q] = []
40
+ errors: list[Exception] = []
41
+ results = await asyncio.gather(*task, return_exceptions=True)
42
+ for item in results:
43
+ # return_exceptions will also catch Python BaseException which also includes KeyboardInterrupt, SystemExit, GeneratorExit
44
+ # https://docs.python.org/3/library/asyncio-task.html#asyncio.gather
45
+ # These exceptions should be raised and not caught for the application to exit properly.
46
+ # https://stackoverflow.com/a/17802352
47
+ if isinstance(item, BaseException):
48
+ raise item
49
+ elif isinstance(item, Exception):
50
+ errors.append(item)
51
+ elif not result_threshold_validation or result_threshold_validation(item):
52
+ valid_items.append(item)
53
+
54
+ return valid_items, errors
55
+
56
+
31
57
  def get_port_diff(
32
58
  before: Iterable[Entity],
33
59
  after: Iterable[Entity],
@@ -11,14 +11,18 @@ async def _start_processor_worker(
11
11
  queue: Queue[Any | None],
12
12
  func: Callable[..., Coroutine[Any, Any, T]],
13
13
  results: list[T],
14
+ errors: list[Exception],
14
15
  ) -> None:
15
16
  while True:
16
- raw_params = await queue.get()
17
17
  try:
18
+ raw_params = await queue.get()
18
19
  if raw_params is None:
19
20
  return
20
- logger.debug(f"Processing {raw_params[0]}")
21
+ logger.debug("Processing async task")
21
22
  results.append(await func(*raw_params))
23
+ except Exception as e:
24
+ logger.error(f"Error processing task: {e}")
25
+ errors.append(e)
22
26
  finally:
23
27
  queue.task_done()
24
28
 
@@ -59,11 +63,12 @@ async def process_in_queue(
59
63
  queue: Queue[Any | None] = Queue(maxsize=concurrency * 2)
60
64
  tasks: list[Task[Any]] = []
61
65
  processing_results: list[T] = []
66
+ errors: list[Exception] = []
62
67
 
63
68
  for i in range(concurrency):
64
69
  tasks.append(
65
70
  asyncio.create_task(
66
- _start_processor_worker(queue, func, processing_results)
71
+ _start_processor_worker(queue, func, processing_results, errors)
67
72
  )
68
73
  )
69
74
 
@@ -77,5 +82,7 @@ async def process_in_queue(
77
82
 
78
83
  await queue.join()
79
84
  await asyncio.gather(*tasks)
85
+ if errors:
86
+ raise ExceptionGroup("Error processing tasks", errors)
80
87
 
81
88
  return processing_results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.5.22
3
+ Version: 0.5.24
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
@@ -22,7 +22,7 @@ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore,sha256=
22
22
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore,sha256=kCpRPdl3S_jqYYZaOrc0-xa6-l3KqVjNRXc6jCkd_-Q,12
23
23
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml,sha256=jTYvu-Iayl-bxc917Y7ejcC9KyvH-LSq4-bdYZCYsuM,457
24
24
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md,sha256=nzAmB0Bjnd2eZo79OjrlyVOdpTBHTmTxvO7c2C8Q-VQ,292
25
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile,sha256=Hh1dBnL959V2n28pmqFpXSrNvSMQjX6fDCUos8ITiu0,326
25
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile,sha256=LsH3vZqqEJkzeQG44cE7JkvPAuh_WPSqYam4YoMvG3M,328
26
26
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile,sha256=kTa72qEp8pi-joOH6zl8oeIgjEHSCF628p2074yHHNA,1779
27
27
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md,sha256=5VZmgDRW9gO4d8UuzkujslOIDfIDBiAGL2Hd74HK770,468
28
28
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore,sha256=kCpRPdl3S_jqYYZaOrc0-xa6-l3KqVjNRXc6jCkd_-Q,12
@@ -39,7 +39,7 @@ port_ocean/clients/port/authentication.py,sha256=t3z6h4vld-Tzkpth15sstaMJg0rccX-
39
39
  port_ocean/clients/port/client.py,sha256=3GYCM0ZkX3pB6sNoOb-7_6dm0Jr5_vqhflD9iltf_As,2640
40
40
  port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  port_ocean/clients/port/mixins/blueprints.py,sha256=BiqkhvDFdkySWgL1NHI-LAQ9ieZWazZAojPo9E8d7U4,4575
42
- port_ocean/clients/port/mixins/entities.py,sha256=Lg5Sa6jQuhDTQKLURVavqXlBQt4-XPUUigB9JqQ1X0k,7364
42
+ port_ocean/clients/port/mixins/entities.py,sha256=pMERHqy1keb5w2k2xQsNuyZaKSAZ5ijVA4pzxEbAatY,7365
43
43
  port_ocean/clients/port/mixins/integrations.py,sha256=ypRYi7_VlnP2YEPgE8zS-_6WgPzUmsRi95K_pwVIpKE,5943
44
44
  port_ocean/clients/port/mixins/migrations.py,sha256=A6896oJF6WbFL2WroyTkMzr12yhVyWqGoq9dtLNSKBY,1457
45
45
  port_ocean/clients/port/retry_transport.py,sha256=PtIZOAZ6V-ncpVysRUsPOgt8Sf01QLnTKB5YeKBxkJk,1861
@@ -59,7 +59,7 @@ port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  port_ocean/core/defaults/__init__.py,sha256=8qCZg8n06WAdMu9s_FiRtDYLGPGHbOuS60vapeUoAks,142
60
60
  port_ocean/core/defaults/clean.py,sha256=S3UAfca-oU89WJKIB4OgGjGjPr0vxBQ2aRZsLTZhQ04,2185
61
61
  port_ocean/core/defaults/common.py,sha256=QnLFTkT3yIWIRtLQb7fUvvfe5AfInYJy0q5LjlzHkOw,3553
62
- port_ocean/core/defaults/initialize.py,sha256=z7IdE_WN1_gF5_60OwSlid9-di7pYOJhzX8VdFjkXdc,7702
62
+ port_ocean/core/defaults/initialize.py,sha256=Cm1QFO7CLoPDdbJVYycbkWlfJfq7hlGbdRj49clBNUM,7432
63
63
  port_ocean/core/event_listener/__init__.py,sha256=mzJ33wRq0kh60fpVdOHVmvMTUQIvz3vxmifyBgwDn0E,889
64
64
  port_ocean/core/event_listener/base.py,sha256=4RujgPz4VfDFlviu4qLGJFnJougSCL-Ewf0rfTQfwgc,1133
65
65
  port_ocean/core/event_listener/factory.py,sha256=AYYfSHPAF7P5H-uQECXT0JVJjKDHrYkWJJBSL4mGkg8,3697
@@ -67,7 +67,7 @@ port_ocean/core/event_listener/http.py,sha256=UjONC_OODHjpGjvsBPAvO6zGzosdmv5Hx9
67
67
  port_ocean/core/event_listener/kafka.py,sha256=0U_TwmlmtS8N2lprkCmnyOmdqOAvghWxHhSfyu46kEs,6875
68
68
  port_ocean/core/event_listener/once.py,sha256=KdvUqjIcyp8XqhY1GfR_KYJfParFIRrjCtcMf3JMegk,2150
69
69
  port_ocean/core/event_listener/polling.py,sha256=3UxjgQJly5y-hA8R798oFWb7bFsYMxSc6GRozA3biiM,3539
70
- port_ocean/core/handlers/__init__.py,sha256=R2HIRD8JTzupY4mXuXveMBQbuiPiiB3Oivmpc5C2jjI,610
70
+ port_ocean/core/handlers/__init__.py,sha256=d7ShmS90gLRzGKJA6oNy2Zs_dF2yjkmYZInRhBnO9Rw,572
71
71
  port_ocean/core/handlers/base.py,sha256=cTarblazu8yh8xz2FpB-dzDKuXxtoi143XJgPbV_DcM,157
72
72
  port_ocean/core/handlers/entities_state_applier/__init__.py,sha256=kgLZDCeCEzi4r-0nzW9k78haOZNf6PX7mJOUr34A4c8,173
73
73
  port_ocean/core/handlers/entities_state_applier/base.py,sha256=FMsrBOVgaO4o7B1klLDY8fobTUDvyrerCKCICyYtkXs,2193
@@ -76,8 +76,8 @@ port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=G6RyPgBAy
76
76
  port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py,sha256=1zncwCbE-Gej0xaWKlzZgoXxOBe9bgs_YxlZ8QW3NdI,1751
77
77
  port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=82BvU8t5w9uhsxX8hbnwuRPuWhW3cMeuT_5sVIkip1I,1550
78
78
  port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybem9MthOs7p1Wawac87uSXz9U8,156
79
- port_ocean/core/handlers/entity_processor/base.py,sha256=3e5QHlSjE7CCAv0WFSyQ7AhFEOS6-9eHj-ZYXGuvqFQ,2216
80
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=mF3ZyO1aSVxfefoeXSlTaIAFdoqITdsEuxlo2d1dUxM,6913
79
+ port_ocean/core/handlers/entity_processor/base.py,sha256=udR0w5TstTOS5xOfTjAZIEdldn4xr6Oyb3DylatYX3Q,1869
80
+ port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=CLg3FxHDs-GIXmRFC0r_BlJd9VGIi9ySjbWkYfB73i8,7330
81
81
  port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iEgEeXtn2pRMrT4Dze5r1Ixbk,134
82
82
  port_ocean/core/handlers/port_app_config/api.py,sha256=6VbKPwFzsWG0IYsVD81hxSmfqtHUFqrfUuj1DBX5g4w,853
83
83
  port_ocean/core/handlers/port_app_config/base.py,sha256=oufdNLzUmEgJY5PgIz75zDlowWrjA-y8WR4UnM58E58,2897
@@ -88,11 +88,11 @@ port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4
88
88
  port_ocean/core/integrations/mixins/events.py,sha256=Ddfx2L4FpghV38waF8OfVeOV0bHBxNIgjU-q5ffillI,2341
89
89
  port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
90
90
  port_ocean/core/integrations/mixins/sync.py,sha256=TKqRytxXONVhuCo3CB3rDvWNbITnZz33TYTDs3SWWVk,3880
91
- port_ocean/core/integrations/mixins/sync_raw.py,sha256=DQ5pNqUedYXXykuMqecMhqi0GwMAvMWUZsTcjmkycxY,16137
91
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=GmH5YPVnYO6mKEhoRer9F-vYy8mxPspaSWw2y9JJhgc,17954
92
92
  port_ocean/core/integrations/mixins/utils.py,sha256=7y1rGETZIjOQadyIjFJXIHKkQFKx_SwiP-TrAIsyyLY,2303
93
- port_ocean/core/models.py,sha256=bDO_I4Yd33TEZIh2QSV8UwXQIuwE7IgrINkYDHI0dkc,714
94
- port_ocean/core/ocean_types.py,sha256=ltnn22eRuDMFW02kIgmIAu6S06-i9jJV2NJ-MZcwwj0,879
95
- port_ocean/core/utils.py,sha256=6rYtsb1bjW8owxWngGiV3awMLZkP3tXZdxXClmRD1SU,1824
93
+ port_ocean/core/models.py,sha256=YPIU7V3GKeDXIVkNzZn0w17YN2Akl-D8nFs-l4bEKcU,1143
94
+ port_ocean/core/ocean_types.py,sha256=3_d8-n626f1kWLQ_Jxw194LEyrOVupz05qs_Y1pvB-A,990
95
+ port_ocean/core/utils.py,sha256=rP8Fo2TSdSye4w0S8W0Vi4lpMJUOIbZ9kmtCkbGKxPY,2855
96
96
  port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  port_ocean/exceptions/api.py,sha256=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
98
98
  port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
@@ -118,12 +118,12 @@ port_ocean/utils/async_http.py,sha256=arnH458TExn2Dju_Sy6pHas_vF5RMWnOp-jBz5WAAc
118
118
  port_ocean/utils/async_iterators.py,sha256=buFBiPdsqkNMCk91h6ZG8hJa181j7RjgHajbfgeB8A8,1608
119
119
  port_ocean/utils/cache.py,sha256=3KItZDE2yVrbVDr-hoM8lNna8s2dlpxhP4ICdLjH4LQ,2231
120
120
  port_ocean/utils/misc.py,sha256=WZjrEDRfyeqbesVt_Nkp2yjazbKF-BOnxJMFAI721yQ,1965
121
- port_ocean/utils/queue_utils.py,sha256=Pzb6e8PcjylZpXcb9EEIK-QcTty_E2k1egMiJF5J_8Q,2500
121
+ port_ocean/utils/queue_utils.py,sha256=KWWl8YVnG-glcfIHhM6nefY-2sou_C6DVP1VynQwzB4,2762
122
122
  port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,3231
123
123
  port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
124
124
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
125
- port_ocean-0.5.22.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
126
- port_ocean-0.5.22.dist-info/METADATA,sha256=zWwXPhSqwMtx0mAMEKtFtIXtOMW8612WjalUu0WeQHQ,6554
127
- port_ocean-0.5.22.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
128
- port_ocean-0.5.22.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
129
- port_ocean-0.5.22.dist-info/RECORD,,
125
+ port_ocean-0.5.24.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
126
+ port_ocean-0.5.24.dist-info/METADATA,sha256=5EZHZqQ-zCeTf4LVLveW1-L-35tCSQ745mYQJ_FmTCM,6554
127
+ port_ocean-0.5.24.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
128
+ port_ocean-0.5.24.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
129
+ port_ocean-0.5.24.dist-info/RECORD,,