port-ocean 0.5.17__py3-none-any.whl → 0.5.19__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.

@@ -36,6 +36,7 @@ class PortSettings(BaseOceanModel, extra=Extra.allow):
36
36
  client_id: str = Field(..., sensitive=True)
37
37
  client_secret: str = Field(..., sensitive=True)
38
38
  base_url: AnyHttpUrl = parse_obj_as(AnyHttpUrl, "https://api.getport.io")
39
+ port_app_config_cache_ttl: int = 60
39
40
 
40
41
 
41
42
  class IntegrationSettings(BaseOceanModel, extra=Extra.allow):
@@ -14,6 +14,7 @@ from port_ocean.core.ocean_types import (
14
14
  EntitySelectorDiff,
15
15
  )
16
16
  from port_ocean.exceptions.core import EntityProcessorException
17
+ from port_ocean.utils.queue_utils import process_in_queue
17
18
 
18
19
 
19
20
  class JQEntityProcessor(BaseEntityProcessor):
@@ -128,23 +129,19 @@ class JQEntityProcessor(BaseEntityProcessor):
128
129
  raw_entity_mappings: dict[str, Any] = mapping.port.entity.mappings.dict(
129
130
  exclude_unset=True
130
131
  )
131
- calculate_entities_tasks = [
132
- asyncio.create_task(
133
- self._calculate_entity(
134
- data,
135
- raw_entity_mappings,
136
- mapping.port.items_to_parse,
137
- mapping.selector.query,
138
- parse_all,
139
- )
140
- )
141
- for data in raw_results
142
- ]
143
- calculate_entities_results = await asyncio.gather(*calculate_entities_tasks)
132
+
133
+ calculated_entities_results = await process_in_queue(
134
+ raw_results,
135
+ self._calculate_entity,
136
+ raw_entity_mappings,
137
+ mapping.port.items_to_parse,
138
+ mapping.selector.query,
139
+ parse_all,
140
+ )
144
141
 
145
142
  passed_entities = []
146
143
  failed_entities = []
147
- for entities_results in calculate_entities_results:
144
+ for entities_results in calculated_entities_results:
148
145
  for entity, did_entity_pass_selector in entities_results:
149
146
  if entity.get("identifier") and entity.get("blueprint"):
150
147
  parsed_entity = Entity.parse_obj(entity)
@@ -5,8 +5,36 @@ from loguru import logger
5
5
  from pydantic import ValidationError
6
6
 
7
7
  from port_ocean.context.event import event
8
+ from port_ocean.context.ocean import PortOceanContext
8
9
  from port_ocean.core.handlers.base import BaseHandler
9
10
  from port_ocean.core.handlers.port_app_config.models import PortAppConfig
11
+ from port_ocean.utils.misc import get_time
12
+
13
+
14
+ class PortAppConfigCache:
15
+ _port_app_config: PortAppConfig | None
16
+ _retrieval_time: float
17
+
18
+ def __init__(self, cache_ttl: int):
19
+ self._cache_ttl = cache_ttl
20
+
21
+ @property
22
+ def port_app_config(self) -> PortAppConfig:
23
+ if self._port_app_config is None:
24
+ raise ValueError("Port app config is not set")
25
+ return self._port_app_config
26
+
27
+ @port_app_config.setter
28
+ def port_app_config(self, value: PortAppConfig) -> None:
29
+ self._retrieval_time = get_time()
30
+ self._port_app_config = value
31
+
32
+ @property
33
+ def is_cache_invalid(self) -> bool:
34
+ return (
35
+ not self._port_app_config
36
+ or self._retrieval_time + self._cache_ttl < get_time()
37
+ )
10
38
 
11
39
 
12
40
  class BasePortAppConfig(BaseHandler):
@@ -21,24 +49,34 @@ class BasePortAppConfig(BaseHandler):
21
49
 
22
50
  CONFIG_CLASS: Type[PortAppConfig] = PortAppConfig
23
51
 
52
+ def __init__(self, context: PortOceanContext):
53
+ super().__init__(context)
54
+ self._app_config_cache = PortAppConfigCache(
55
+ self.context.config.port.port_app_config_cache_ttl
56
+ )
57
+
24
58
  @abstractmethod
25
59
  async def _get_port_app_config(self) -> dict[str, Any]:
26
60
  pass
27
61
 
28
- async def get_port_app_config(self) -> PortAppConfig:
29
- """Retrieve and parse the port application configuration.
62
+ async def get_port_app_config(self, use_cache: bool = True) -> PortAppConfig:
63
+ """
64
+ Retrieve and parse the port application configuration.
30
65
 
31
- Returns:
32
- PortAppConfig: The parsed port application configuration.
66
+ :param use_cache: Determines whether to use the cached port-app-config if it exists, or to fetch it regardless
67
+ :return: The parsed port application configuration.
33
68
  """
34
- raw_config = await self._get_port_app_config()
35
- try:
36
- config = self.CONFIG_CLASS.parse_obj(raw_config)
37
- except ValidationError:
38
- logger.error(
39
- "Invalid port app config found. Please check that the integration has been configured correctly."
40
- )
41
- raise
42
-
43
- event.port_app_config = config
44
- return config
69
+ if not use_cache or self._app_config_cache.is_cache_invalid:
70
+ raw_config = await self._get_port_app_config()
71
+ try:
72
+ self._app_config_cache.port_app_config = self.CONFIG_CLASS.parse_obj(
73
+ raw_config
74
+ )
75
+ except ValidationError:
76
+ logger.error(
77
+ "Invalid port app config found. Please check that the integration has been configured correctly."
78
+ )
79
+ raise
80
+
81
+ event.port_app_config = self._app_config_cache.port_app_config
82
+ return self._app_config_cache.port_app_config
@@ -362,7 +362,11 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
362
362
  EventType.RESYNC,
363
363
  trigger_type=trigger_type,
364
364
  ):
365
- app_config = await self.port_app_config_handler.get_port_app_config()
365
+ # If a resync is triggered due to a mappings change, we want to make sure that we have the updated version
366
+ # rather than the old cache
367
+ app_config = await self.port_app_config_handler.get_port_app_config(
368
+ use_cache=False
369
+ )
366
370
 
367
371
  creation_results: list[tuple[list[Entity], list[Exception]]] = []
368
372
 
port_ocean/core/utils.py CHANGED
@@ -28,38 +28,34 @@ def is_same_entity(first_entity: Entity, second_entity: Entity) -> bool:
28
28
  )
29
29
 
30
30
 
31
- def get_unique(array: list[Entity]) -> list[Entity]:
32
- result: list[Entity] = []
33
- for item in array:
34
- if all(not is_same_entity(item, seen_item) for seen_item in result):
35
- result.append(item)
36
- return result
37
-
38
-
39
31
  def get_port_diff(
40
32
  before: Iterable[Entity],
41
33
  after: Iterable[Entity],
42
34
  ) -> EntityPortDiff:
43
- return EntityPortDiff(
44
- deleted=get_unique(
45
- [
46
- item
47
- for item in before
48
- if not any(is_same_entity(item, item_after) for item_after in after)
49
- ],
50
- ),
51
- created=get_unique(
52
- [
53
- item
54
- for item in after
55
- if not any(is_same_entity(item, item_before) for item_before in before)
56
- ],
57
- ),
58
- modified=get_unique(
59
- [
60
- item
61
- for item in after
62
- if any(is_same_entity(item, item_before) for item_before in before)
63
- ],
64
- ),
65
- )
35
+ before_dict = {}
36
+ after_dict = {}
37
+ created = []
38
+ modified = []
39
+ deleted = []
40
+
41
+ # Create dictionaries for before and after lists
42
+ for entity in before:
43
+ key = (entity.identifier, entity.blueprint)
44
+ before_dict[key] = entity
45
+
46
+ for entity in after:
47
+ key = (entity.identifier, entity.blueprint)
48
+ after_dict[key] = entity
49
+
50
+ # Find created, modified, and deleted objects
51
+ for key, obj in after_dict.items():
52
+ if key not in before_dict:
53
+ created.append(obj)
54
+ else:
55
+ modified.append(obj)
56
+
57
+ for key, obj in before_dict.items():
58
+ if key not in after_dict:
59
+ deleted.append(obj)
60
+
61
+ return EntityPortDiff(created=created, modified=modified, deleted=deleted)
@@ -0,0 +1,81 @@
1
+ import asyncio
2
+ from asyncio import Queue, Task
3
+ from typing import Any, TypeVar, Callable, Coroutine
4
+
5
+ from loguru import logger
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ async def _start_processor_worker(
11
+ queue: Queue[Any | None],
12
+ func: Callable[..., Coroutine[Any, Any, T]],
13
+ results: list[T],
14
+ ) -> None:
15
+ while True:
16
+ raw_params = await queue.get()
17
+ try:
18
+ if raw_params is None:
19
+ return
20
+ logger.debug(f"Processing {raw_params[0]}")
21
+ results.append(await func(*raw_params))
22
+ finally:
23
+ queue.task_done()
24
+
25
+
26
+ async def process_in_queue(
27
+ objects_to_process: list[Any],
28
+ func: Callable[..., Coroutine[Any, Any, T]],
29
+ *args: Any,
30
+ concurrency: int = 50,
31
+ ) -> list[T]:
32
+ """
33
+ This function executes multiple asynchronous tasks in a bounded way
34
+ (e.g. having 200 tasks to execute, while running only 20 concurrently),
35
+ to prevent overload and memory issues when dealing with large sets of data and tasks.
36
+ read more -> https://stackoverflow.com/questions/38831322/making-1-milion-requests-with-aiohttp-asyncio-literally
37
+
38
+ Usage:
39
+ ```python
40
+ async def incrementBy(num: int, increment_by: int) -> int:
41
+ await asyncio.sleep(3)
42
+ return num + increment_by
43
+
44
+ async def main():
45
+ raw_objects = [1, 2, 3, 4, 5]
46
+ processed_objects = await process_in_queue(
47
+ raw_objects,
48
+ incrementBy,
49
+ 5
50
+ )
51
+ ```
52
+
53
+ :param objects_to_process: A list of the raw objects to process
54
+ :param func: An async function that turns raw object into result object
55
+ :param args: Static arguments to pass to the func when called
56
+ :param concurrency: An integer specifying the concurrent workers count
57
+ :return: A list of result objects
58
+ """
59
+ queue: Queue[Any | None] = Queue(maxsize=concurrency * 2)
60
+ tasks: list[Task[Any]] = []
61
+ processing_results: list[T] = []
62
+
63
+ for i in range(concurrency):
64
+ tasks.append(
65
+ asyncio.create_task(
66
+ _start_processor_worker(queue, func, processing_results)
67
+ )
68
+ )
69
+
70
+ for i in range(len(objects_to_process)):
71
+ await queue.put((objects_to_process[i], *args))
72
+
73
+ for i in range(concurrency):
74
+ # We put None value into the queue, so the workers will know that we
75
+ # are done sending more input and they can terminate
76
+ await queue.put(None)
77
+
78
+ await queue.join()
79
+ await asyncio.gather(*tasks)
80
+
81
+ return processing_results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.5.17
3
+ Version: 0.5.19
4
4
  Summary: Port Ocean is a CLI tool for managing your Port projects.
5
5
  Home-page: https://app.getport.io
6
6
  Keywords: ocean,port-ocean,port
@@ -49,7 +49,7 @@ port_ocean/clients/port/utils.py,sha256=O9mBu6zp4TfpS4SQ3qCPpn9ZVyYF8GKnji4UnYhM
49
49
  port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  port_ocean/config/base.py,sha256=8JoyjTPJS_BeYMnGnKN_q4SDaI-ydctuKl9ccK-Uq78,6474
51
51
  port_ocean/config/dynamic.py,sha256=qOFkRoJsn_BW7581omi_AoMxoHqasf_foxDQ_G11_SI,2030
52
- port_ocean/config/settings.py,sha256=5bQmOAIZ2IMDzUEsiVkfZokv9LxHLPOUVKA0TPoJh68,1769
52
+ port_ocean/config/settings.py,sha256=7eJSuOQU69NWrxUfy6UnSdL9aZQ_nu9VOp0-XHy9-Ds,1809
53
53
  port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
55
55
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -78,10 +78,10 @@ port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py,sha
78
78
  port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=82BvU8t5w9uhsxX8hbnwuRPuWhW3cMeuT_5sVIkip1I,1550
79
79
  port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybem9MthOs7p1Wawac87uSXz9U8,156
80
80
  port_ocean/core/handlers/entity_processor/base.py,sha256=4JVCAAohEKtl8FdlnuyIxJ1afSXk3o2-e_m4LSy7vmw,1952
81
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=R5PwTcsJYRVU-lwoYZa1Af8kserp2BXIpyzVtRdEqA8,5617
81
+ port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=FDpMeBntu5x-p2DGuk8BpQVe1zy7afJ93nWfrLnZPRc,5479
82
82
  port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iEgEeXtn2pRMrT4Dze5r1Ixbk,134
83
83
  port_ocean/core/handlers/port_app_config/api.py,sha256=6VbKPwFzsWG0IYsVD81hxSmfqtHUFqrfUuj1DBX5g4w,853
84
- port_ocean/core/handlers/port_app_config/base.py,sha256=nnMZ4jH6a-4Of9Cn-apMsU0CgNLD9avd5q0gRmc7nZ8,1495
84
+ port_ocean/core/handlers/port_app_config/base.py,sha256=oufdNLzUmEgJY5PgIz75zDlowWrjA-y8WR4UnM58E58,2897
85
85
  port_ocean/core/handlers/port_app_config/models.py,sha256=WtF2uW4KPUPfDpy6E2tl9qXh5GUcDucVvKt0DCTYv6w,1985
86
86
  port_ocean/core/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
87
  port_ocean/core/integrations/base.py,sha256=3jU0skK_gMLB8a_fN8whsRKva-Dz058TFoI0vXTbryU,3193
@@ -89,11 +89,11 @@ port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4
89
89
  port_ocean/core/integrations/mixins/events.py,sha256=Ddfx2L4FpghV38waF8OfVeOV0bHBxNIgjU-q5ffillI,2341
90
90
  port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
91
91
  port_ocean/core/integrations/mixins/sync.py,sha256=TKqRytxXONVhuCo3CB3rDvWNbITnZz33TYTDs3SWWVk,3880
92
- port_ocean/core/integrations/mixins/sync_raw.py,sha256=kWZ44L2M2G6B0n3oOqF1Ko9Rct06CXnTnWY25Q2eh3c,15049
92
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=iWI8fXd36TwUNFDAgEJj4SGvWwDOQsez1sQpc2EriKQ,15253
93
93
  port_ocean/core/integrations/mixins/utils.py,sha256=7y1rGETZIjOQadyIjFJXIHKkQFKx_SwiP-TrAIsyyLY,2303
94
94
  port_ocean/core/models.py,sha256=bDO_I4Yd33TEZIh2QSV8UwXQIuwE7IgrINkYDHI0dkc,714
95
95
  port_ocean/core/ocean_types.py,sha256=ltnn22eRuDMFW02kIgmIAu6S06-i9jJV2NJ-MZcwwj0,879
96
- port_ocean/core/utils.py,sha256=B040Wokk28g9tQj_06qk_uvm85RIXc6XGXysZV6gtQw,1957
96
+ port_ocean/core/utils.py,sha256=6rYtsb1bjW8owxWngGiV3awMLZkP3tXZdxXClmRD1SU,1824
97
97
  port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  port_ocean/exceptions/api.py,sha256=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
99
99
  port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
@@ -119,11 +119,12 @@ port_ocean/utils/async_http.py,sha256=arnH458TExn2Dju_Sy6pHas_vF5RMWnOp-jBz5WAAc
119
119
  port_ocean/utils/async_iterators.py,sha256=buFBiPdsqkNMCk91h6ZG8hJa181j7RjgHajbfgeB8A8,1608
120
120
  port_ocean/utils/cache.py,sha256=3KItZDE2yVrbVDr-hoM8lNna8s2dlpxhP4ICdLjH4LQ,2231
121
121
  port_ocean/utils/misc.py,sha256=2XmO8W0SgPjV0rd9HZvrHhoMlHprIwmMFsINxlAmgyw,1723
122
+ port_ocean/utils/queue_utils.py,sha256=Pzb6e8PcjylZpXcb9EEIK-QcTty_E2k1egMiJF5J_8Q,2500
122
123
  port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,3231
123
124
  port_ocean/utils/signal.py,sha256=Fab0049Cjs69TPTQgvEvilaVZKACQr6tGkRdySjNCi8,1515
124
125
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
125
- port_ocean-0.5.17.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
126
- port_ocean-0.5.17.dist-info/METADATA,sha256=uaVWJBt0vRCLiqO22dU72mqY-rnoBZgXRKJvmCCKFzE,6554
127
- port_ocean-0.5.17.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
128
- port_ocean-0.5.17.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
129
- port_ocean-0.5.17.dist-info/RECORD,,
126
+ port_ocean-0.5.19.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
127
+ port_ocean-0.5.19.dist-info/METADATA,sha256=m3voj8mZP19CPHPXnBDLx58G07P5kfxDuCWAjvu2wTE,6554
128
+ port_ocean-0.5.19.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
129
+ port_ocean-0.5.19.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
130
+ port_ocean-0.5.19.dist-info/RECORD,,