port-ocean 0.28.5__py3-none-any.whl → 0.29.0__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.
Files changed (51) hide show
  1. integrations/_infra/Dockerfile.Deb +1 -0
  2. integrations/_infra/Dockerfile.local +1 -0
  3. port_ocean/clients/port/authentication.py +19 -0
  4. port_ocean/clients/port/client.py +3 -0
  5. port_ocean/clients/port/mixins/actions.py +93 -0
  6. port_ocean/clients/port/mixins/blueprints.py +0 -12
  7. port_ocean/clients/port/mixins/entities.py +79 -44
  8. port_ocean/clients/port/mixins/integrations.py +7 -2
  9. port_ocean/config/settings.py +35 -3
  10. port_ocean/context/ocean.py +7 -5
  11. port_ocean/core/defaults/initialize.py +12 -5
  12. port_ocean/core/event_listener/__init__.py +7 -0
  13. port_ocean/core/event_listener/actions_only.py +42 -0
  14. port_ocean/core/event_listener/base.py +4 -1
  15. port_ocean/core/event_listener/factory.py +18 -9
  16. port_ocean/core/event_listener/http.py +4 -3
  17. port_ocean/core/event_listener/kafka.py +3 -2
  18. port_ocean/core/event_listener/once.py +5 -2
  19. port_ocean/core/event_listener/polling.py +4 -3
  20. port_ocean/core/event_listener/webhooks_only.py +3 -2
  21. port_ocean/core/handlers/actions/__init__.py +7 -0
  22. port_ocean/core/handlers/actions/abstract_executor.py +150 -0
  23. port_ocean/core/handlers/actions/execution_manager.py +434 -0
  24. port_ocean/core/handlers/entity_processor/jq_entity_processor.py +479 -17
  25. port_ocean/core/handlers/entity_processor/jq_input_evaluator.py +137 -0
  26. port_ocean/core/handlers/port_app_config/models.py +4 -2
  27. port_ocean/core/handlers/webhook/abstract_webhook_processor.py +16 -0
  28. port_ocean/core/handlers/webhook/processor_manager.py +30 -12
  29. port_ocean/core/integrations/mixins/sync_raw.py +4 -4
  30. port_ocean/core/integrations/mixins/utils.py +250 -29
  31. port_ocean/core/models.py +35 -2
  32. port_ocean/core/utils/utils.py +16 -5
  33. port_ocean/exceptions/execution_manager.py +22 -0
  34. port_ocean/helpers/retry.py +4 -40
  35. port_ocean/log/logger_setup.py +2 -2
  36. port_ocean/ocean.py +30 -4
  37. port_ocean/tests/clients/port/mixins/test_entities.py +71 -5
  38. port_ocean/tests/core/event_listener/test_kafka.py +14 -7
  39. port_ocean/tests/core/handlers/actions/test_execution_manager.py +837 -0
  40. port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +932 -1
  41. port_ocean/tests/core/handlers/entity_processor/test_jq_input_evaluator.py +932 -0
  42. port_ocean/tests/core/handlers/webhook/test_processor_manager.py +3 -1
  43. port_ocean/tests/core/utils/test_get_port_diff.py +164 -0
  44. port_ocean/tests/helpers/test_retry.py +241 -1
  45. port_ocean/tests/utils/test_cache.py +240 -0
  46. port_ocean/utils/cache.py +45 -9
  47. {port_ocean-0.28.5.dist-info → port_ocean-0.29.0.dist-info}/METADATA +2 -1
  48. {port_ocean-0.28.5.dist-info → port_ocean-0.29.0.dist-info}/RECORD +51 -41
  49. {port_ocean-0.28.5.dist-info → port_ocean-0.29.0.dist-info}/LICENSE.md +0 -0
  50. {port_ocean-0.28.5.dist-info → port_ocean-0.29.0.dist-info}/WHEEL +0 -0
  51. {port_ocean-0.28.5.dist-info → port_ocean-0.29.0.dist-info}/entry_points.txt +0 -0
@@ -48,6 +48,7 @@ RUN apt-get update \
48
48
  curl \
49
49
  acl \
50
50
  sudo \
51
+ jq \
51
52
  && apt-get clean
52
53
 
53
54
  LABEL INTEGRATION_VERSION=${INTEGRATION_VERSION}
@@ -26,6 +26,7 @@ RUN apt-get update \
26
26
  python3-pip \
27
27
  python3-poetry \
28
28
  build-essential\
29
+ jq \
29
30
  git \
30
31
  python3-venv \
31
32
  acl \
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import jwt
2
3
  from typing import Any
3
4
 
4
5
  import httpx
@@ -90,6 +91,24 @@ class PortAuthentication:
90
91
  )
91
92
  return self.last_token_object.full_token
92
93
 
94
+ async def is_machine_user(self) -> bool:
95
+ # Ensure self.last_token_object is populated
96
+ await self.token
97
+ if not self.last_token_object:
98
+ raise ValueError("No token found")
99
+
100
+ payload: dict[str, Any] = jwt.decode(
101
+ self.last_token_object.access_token, options={"verify_signature": False}
102
+ )
103
+ is_machine_user = payload.get("isMachine")
104
+ if is_machine_user is None:
105
+ logger.warning(
106
+ "Can not determine if the user is a machine user directly, checking for personal token usage instead"
107
+ )
108
+ return not payload.get("personalToken", True)
109
+
110
+ return is_machine_user
111
+
93
112
  @staticmethod
94
113
  def _is_personal_token(client_id: str) -> bool:
95
114
  email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
@@ -3,6 +3,7 @@ from typing import Any
3
3
  from loguru import logger
4
4
 
5
5
  from port_ocean.clients.port.authentication import PortAuthentication
6
+ from port_ocean.clients.port.mixins.actions import ActionsClientMixin
6
7
  from port_ocean.clients.port.mixins.blueprints import BlueprintClientMixin
7
8
  from port_ocean.clients.port.mixins.entities import EntityClientMixin
8
9
  from port_ocean.clients.port.mixins.integrations import IntegrationClientMixin
@@ -24,6 +25,7 @@ class PortClient(
24
25
  BlueprintClientMixin,
25
26
  MigrationClientMixin,
26
27
  OrganizationClientMixin,
28
+ ActionsClientMixin,
27
29
  ):
28
30
  def __init__(
29
31
  self,
@@ -54,6 +56,7 @@ class PortClient(
54
56
  BlueprintClientMixin.__init__(self, self.auth, self.client)
55
57
  MigrationClientMixin.__init__(self, self.auth, self.client)
56
58
  OrganizationClientMixin.__init__(self, self.auth, self.client)
59
+ ActionsClientMixin.__init__(self, self.auth, self.client)
57
60
 
58
61
  async def get_kafka_creds(self) -> KafkaCreds:
59
62
  logger.info("Fetching organization kafka credentials")
@@ -0,0 +1,93 @@
1
+ from typing import Any
2
+ import httpx
3
+ from loguru import logger
4
+ from port_ocean.clients.port.authentication import PortAuthentication
5
+ from port_ocean.clients.port.utils import handle_port_status_code
6
+ from port_ocean.core.models import (
7
+ ActionRun,
8
+ )
9
+ from port_ocean.exceptions.execution_manager import RunAlreadyAcknowledgedError
10
+
11
+ INTERNAL_ACTIONS_CLIENT_HEADER = {"x-port-automation-client": "true"}
12
+
13
+
14
+ class ActionsClientMixin:
15
+ def __init__(self, auth: PortAuthentication, client: httpx.AsyncClient):
16
+ self.auth = auth
17
+ self.client = client
18
+
19
+ async def create_action(
20
+ self, action: dict[str, Any], should_log: bool = True
21
+ ) -> None:
22
+ logger.info(f"Creating action: {action}")
23
+ response = await self.client.post(
24
+ f"{self.auth.api_url}/actions",
25
+ json=action,
26
+ headers=await self.auth.headers(),
27
+ )
28
+
29
+ handle_port_status_code(response, should_log=should_log)
30
+
31
+ async def claim_pending_runs(
32
+ self, limit: int, visibility_timeout_ms: int
33
+ ) -> list[ActionRun]:
34
+ response = await self.client.post(
35
+ f"{self.auth.api_url}/actions/runs/claim-pending",
36
+ headers={**(await self.auth.headers()), **INTERNAL_ACTIONS_CLIENT_HEADER},
37
+ json={
38
+ "installationId": self.auth.integration_identifier,
39
+ "limit": limit,
40
+ "visibilityTimeoutMs": visibility_timeout_ms,
41
+ },
42
+ )
43
+ if response.is_error:
44
+ logger.error("Error claiming pending runs", error=response.text)
45
+ return []
46
+
47
+ return [ActionRun.parse_obj(run) for run in response.json().get("runs", [])]
48
+
49
+ async def get_run_by_external_id(self, external_id: str) -> ActionRun | None:
50
+ response = await self.client.get(
51
+ f"{self.auth.api_url}/actions/runs?version=v2&external_run_id={external_id}",
52
+ headers=await self.auth.headers(),
53
+ )
54
+ handle_port_status_code(response)
55
+ runs = response.json().get("runs", [])
56
+ return None if not len(runs) else ActionRun.parse_obj(runs[0])
57
+
58
+ async def patch_run(
59
+ self,
60
+ run_id: str,
61
+ run: ActionRun | dict[str, Any],
62
+ should_raise: bool = True,
63
+ ) -> None:
64
+ response = await self.client.patch(
65
+ f"{self.auth.api_url}/actions/runs/{run_id}",
66
+ headers=await self.auth.headers(),
67
+ json=run.dict() if isinstance(run, ActionRun) else run,
68
+ )
69
+ handle_port_status_code(response, should_raise=should_raise)
70
+
71
+ async def acknowledge_run(self, run_id: str) -> None:
72
+ try:
73
+ response = await self.client.patch(
74
+ f"{self.auth.api_url}/actions/runs/ack",
75
+ headers={
76
+ **(await self.auth.headers()),
77
+ **INTERNAL_ACTIONS_CLIENT_HEADER,
78
+ },
79
+ json={"runId": run_id},
80
+ )
81
+ handle_port_status_code(response)
82
+ except httpx.HTTPStatusError as e:
83
+ if e.response.status_code == 409:
84
+ raise RunAlreadyAcknowledgedError()
85
+ raise
86
+
87
+ async def post_run_log(self, run_id: str, message: str) -> None:
88
+ response = await self.client.post(
89
+ f"{self.auth.api_url}/actions/runs/{run_id}/logs",
90
+ headers=await self.auth.headers(),
91
+ json={"message": message},
92
+ )
93
+ handle_port_status_code(response, should_raise=False)
@@ -81,18 +81,6 @@ class BlueprintClientMixin:
81
81
  handle_port_status_code(response, should_raise)
82
82
  return response.json().get("migrationId", "")
83
83
 
84
- async def create_action(
85
- self, action: dict[str, Any], should_log: bool = True
86
- ) -> None:
87
- logger.info(f"Creating action: {action}")
88
- response = await self.client.post(
89
- f"{self.auth.api_url}/actions",
90
- json=action,
91
- headers=await self.auth.headers(),
92
- )
93
-
94
- handle_port_status_code(response, should_log=should_log)
95
-
96
84
  async def create_scorecard(
97
85
  self,
98
86
  blueprint_identifier: str,
@@ -1,24 +1,24 @@
1
1
  import asyncio
2
+ import json
2
3
  from typing import Any, Literal
3
4
  from urllib.parse import quote_plus
4
- import json
5
5
 
6
6
  import httpx
7
7
  from loguru import logger
8
- from port_ocean.context.ocean import ocean
8
+ from starlette import status
9
+
9
10
  from port_ocean.clients.port.authentication import PortAuthentication
10
11
  from port_ocean.clients.port.types import RequestOptions, UserAgentType
11
12
  from port_ocean.clients.port.utils import (
12
- handle_port_status_code,
13
13
  PORT_HTTP_MAX_CONNECTIONS_LIMIT,
14
+ handle_port_status_code,
14
15
  )
16
+ from port_ocean.context.ocean import ocean
15
17
  from port_ocean.core.models import (
16
18
  BulkUpsertResponse,
17
19
  Entity,
18
20
  PortAPIErrorMessage,
19
21
  )
20
- from starlette import status
21
-
22
22
  from port_ocean.helpers.metric.metric import MetricPhase, MetricType
23
23
 
24
24
  ENTITIES_BULK_SAMPLES_SIZE = 10
@@ -484,54 +484,89 @@ class EntityClientMixin:
484
484
  parameters_to_include: list[str] | None = None,
485
485
  ) -> list[Entity]:
486
486
  if query is None:
487
- datasource_prefix = f"port-ocean/{self.auth.integration_type}/"
488
- datasource_suffix = (
489
- f"/{self.auth.integration_identifier}/{user_agent_type.value}"
490
- )
491
- logger.info(
492
- f"Searching entities with datasource prefix: {datasource_prefix} and suffix: {datasource_suffix}"
493
- )
487
+ return await self._search_entities_by_datasource_paginated(user_agent_type)
488
+
489
+ return await self._search_entities_by_query(
490
+ user_agent_type=user_agent_type,
491
+ query=query,
492
+ parameters_to_include=parameters_to_include,
493
+ )
494
+
495
+ async def _search_entities_by_datasource_paginated(
496
+ self, user_agent_type: UserAgentType
497
+ ) -> list[Entity]:
498
+ datasource_prefix = f"port-ocean/{self.auth.integration_type}/"
499
+ datasource_suffix = (
500
+ f"/{self.auth.integration_identifier}/{user_agent_type.value}"
501
+ )
502
+ logger.info(
503
+ f"Searching entities with datasource prefix: {datasource_prefix} and suffix: {datasource_suffix}"
504
+ )
505
+
506
+ next_from: str | None = None
507
+ aggregated_entities: list[Entity] = []
508
+ while True:
509
+ request_body: dict[str, Any] = {
510
+ "datasource_prefix": datasource_prefix,
511
+ "datasource_suffix": datasource_suffix,
512
+ }
513
+
514
+ if next_from:
515
+ request_body["from"] = next_from
494
516
 
495
517
  response = await self.client.post(
496
518
  f"{self.auth.api_url}/blueprints/entities/datasource-entities",
497
- json={
498
- "datasource_prefix": datasource_prefix,
499
- "datasource_suffix": datasource_suffix,
500
- },
519
+ json=request_body,
501
520
  headers=await self.auth.headers(user_agent_type),
502
521
  extensions={"retryable": True},
503
522
  )
504
- else:
505
- default_query = {
506
- "combinator": "and",
507
- "rules": [
508
- {
509
- "property": "$datasource",
510
- "operator": "contains",
511
- "value": f"port-ocean/{self.auth.integration_type}/",
512
- },
513
- {
514
- "property": "$datasource",
515
- "operator": "contains",
516
- "value": f"/{self.auth.integration_identifier}/{user_agent_type.value}",
517
- },
518
- ],
519
- }
523
+ handle_port_status_code(response)
524
+ response_json = response.json()
525
+ aggregated_entities.extend(
526
+ Entity.parse_obj(result) for result in response_json.get("entities", [])
527
+ )
528
+ next_from = response_json.get("next")
529
+ if not next_from:
530
+ break
520
531
 
521
- if query.get("rules"):
522
- query["rules"].extend(default_query["rules"])
532
+ return aggregated_entities
523
533
 
524
- logger.info(f"Searching entities with custom query: {query}")
525
- response = await self.client.post(
526
- f"{self.auth.api_url}/entities/search",
527
- json=query,
528
- headers=await self.auth.headers(user_agent_type),
529
- params={
530
- "exclude_calculated_properties": "true",
531
- "include": parameters_to_include or ["blueprint", "identifier"],
534
+ async def _search_entities_by_query(
535
+ self,
536
+ user_agent_type: UserAgentType,
537
+ query: dict[Any, Any],
538
+ parameters_to_include: list[str] | None,
539
+ ) -> list[Entity]:
540
+ default_query = {
541
+ "combinator": "and",
542
+ "rules": [
543
+ {
544
+ "property": "$datasource",
545
+ "operator": "contains",
546
+ "value": f"port-ocean/{self.auth.integration_type}/",
532
547
  },
533
- extensions={"retryable": True},
534
- )
548
+ {
549
+ "property": "$datasource",
550
+ "operator": "contains",
551
+ "value": f"/{self.auth.integration_identifier}/{user_agent_type.value}",
552
+ },
553
+ ],
554
+ }
555
+
556
+ if query.get("rules"):
557
+ query["rules"].extend(default_query["rules"])
558
+
559
+ logger.info(f"Searching entities with custom query: {query}")
560
+ response = await self.client.post(
561
+ f"{self.auth.api_url}/entities/search",
562
+ json=query,
563
+ headers=await self.auth.headers(user_agent_type),
564
+ params={
565
+ "exclude_calculated_properties": "true",
566
+ "include": parameters_to_include or ["blueprint", "identifier"],
567
+ },
568
+ extensions={"retryable": True},
569
+ )
535
570
 
536
571
  handle_port_status_code(response)
537
572
  return [Entity.parse_obj(result) for result in response.json()["entities"]]
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from datetime import datetime
2
3
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict
3
4
  from urllib.parse import quote_plus
4
5
 
@@ -13,8 +14,6 @@ from port_ocean.log.sensetive import sensitive_log_filter
13
14
  if TYPE_CHECKING:
14
15
  from port_ocean.core.handlers.port_app_config.models import PortAppConfig
15
16
 
16
-
17
- ORG_USE_PROVISIONED_DEFAULTS_FEATURE_FLAG = "USE_PROVISIONED_DEFAULTS"
18
17
  INTEGRATION_POLLING_INTERVAL_INITIAL_SECONDS = 3
19
18
  INTEGRATION_POLLING_INTERVAL_BACKOFF_FACTOR = 1.55
20
19
  INTEGRATION_POLLING_RETRY_LIMIT = 30
@@ -152,6 +151,7 @@ class IntegrationClientMixin:
152
151
  changelog_destination: dict[str, Any],
153
152
  port_app_config: Optional["PortAppConfig"] = None,
154
153
  create_port_resources_origin_in_port: Optional[bool] = False,
154
+ actions_processing_enabled: Optional[bool] = False,
155
155
  ) -> Dict[str, Any]:
156
156
  logger.info(f"Creating integration with id: {self.integration_identifier}")
157
157
  headers = await self.auth.headers()
@@ -161,6 +161,7 @@ class IntegrationClientMixin:
161
161
  "version": self.integration_version,
162
162
  "changelogDestination": changelog_destination,
163
163
  "config": {},
164
+ "actionsProcessingEnabled": actions_processing_enabled,
164
165
  }
165
166
 
166
167
  query_params = {}
@@ -189,6 +190,7 @@ class IntegrationClientMixin:
189
190
  _type: str | None = None,
190
191
  changelog_destination: dict[str, Any] | None = None,
191
192
  port_app_config: Optional["PortAppConfig"] = None,
193
+ actions_processing_enabled: Optional[bool] = False,
192
194
  ) -> dict:
193
195
  logger.info(f"Updating integration with id: {self.integration_identifier}")
194
196
  headers = await self.auth.headers()
@@ -199,6 +201,8 @@ class IntegrationClientMixin:
199
201
  json["changelogDestination"] = changelog_destination
200
202
  if port_app_config:
201
203
  json["config"] = port_app_config.to_request()
204
+
205
+ json["actionsProcessingEnabled"] = actions_processing_enabled
202
206
  json["version"] = self.integration_version
203
207
 
204
208
  response = await self.client.patch(
@@ -300,6 +304,7 @@ class IntegrationClientMixin:
300
304
  headers=headers,
301
305
  json={
302
306
  "items": raw_data,
307
+ "extractionTimestamp": int(datetime.now().timestamp() * 1000),
303
308
  },
304
309
  )
305
310
  handle_port_status_code(response, should_log=False)
@@ -1,5 +1,5 @@
1
1
  import platform
2
- from typing import Any, Literal, Optional, Type, cast
2
+ from typing import Any, Literal, Optional, Type
3
3
 
4
4
  from pydantic import AnyHttpUrl, Extra, parse_obj_as, parse_raw_as
5
5
  from pydantic.class_validators import root_validator, validator
@@ -8,10 +8,14 @@ from pydantic.fields import Field
8
8
  from pydantic.main import BaseModel
9
9
 
10
10
  from port_ocean.config.base import BaseOceanModel, BaseOceanSettings
11
- from port_ocean.core.event_listener import EventListenerSettingsType
11
+ from port_ocean.core.event_listener import (
12
+ EventListenerSettingsType,
13
+ PollingEventListenerSettings,
14
+ )
12
15
  from port_ocean.core.models import (
13
16
  CachingStorageMode,
14
17
  CreatePortResourcesOrigin,
18
+ EventListenerType,
15
19
  ProcessExecutionMode,
16
20
  Runtime,
17
21
  )
@@ -80,6 +84,14 @@ class StreamingSettings(BaseOceanModel, extra=Extra.allow):
80
84
  location: str = Field(default="/tmp/ocean/streaming")
81
85
 
82
86
 
87
+ class ActionsProcessorSettings(BaseOceanModel, extra=Extra.allow):
88
+ enabled: bool = Field(default=False)
89
+ runs_buffer_high_watermark: int = Field(default=100)
90
+ visibility_timeout_ms: int = Field(default=30000)
91
+ poll_check_interval_seconds: int = Field(default=10)
92
+ workers_count: int = Field(default=1)
93
+
94
+
83
95
  class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
84
96
  _integration_config_model: BaseModel | None = None
85
97
 
@@ -94,7 +106,9 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
94
106
  base_url: str | None = None
95
107
  port: PortSettings
96
108
  event_listener: EventListenerSettingsType = Field(
97
- default=cast(EventListenerSettingsType, {"type": "POLLING"})
109
+ default_factory=lambda: PollingEventListenerSettings(
110
+ type=EventListenerType.POLLING
111
+ )
98
112
  )
99
113
  event_workers_count: int = 1
100
114
  # If an identifier or type is not provided, it will be generated based on the integration name
@@ -122,6 +136,9 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
122
136
  yield_items_to_parse_batch_size: int = 10
123
137
 
124
138
  streaming: StreamingSettings = Field(default_factory=lambda: StreamingSettings())
139
+ actions_processor: ActionsProcessorSettings = Field(
140
+ default_factory=lambda: ActionsProcessorSettings()
141
+ )
125
142
 
126
143
  @validator("process_execution_mode")
127
144
  def validate_process_execution_mode(
@@ -197,3 +214,18 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
197
214
  raise ValueError("This integration can't be ran as Saas")
198
215
 
199
216
  return runtime
217
+
218
+ @validator("actions_processor")
219
+ def validate_actions_processor(
220
+ cls, actions_processor: ActionsProcessorSettings
221
+ ) -> ActionsProcessorSettings:
222
+ if not actions_processor.enabled:
223
+ return actions_processor
224
+
225
+ spec = get_spec_file()
226
+ if not (spec and spec.get("actionsProcessingEnabled", False)):
227
+ raise ValueError(
228
+ "Serving as an actions processor is not currently supported for this integration."
229
+ )
230
+
231
+ return actions_processor
@@ -1,4 +1,4 @@
1
- from typing import Callable, TYPE_CHECKING, Any, Literal, Union
1
+ from typing import Callable, TYPE_CHECKING, Any, Union
2
2
 
3
3
  from fastapi import APIRouter
4
4
  from port_ocean.helpers.metric.metric import Metrics
@@ -7,7 +7,7 @@ from werkzeug.local import LocalProxy
7
7
 
8
8
  from port_ocean.clients.port.types import UserAgentType
9
9
 
10
- from port_ocean.core.models import Entity
10
+ from port_ocean.core.models import Entity, EventListenerType
11
11
  from port_ocean.core.ocean_types import (
12
12
  RESYNC_EVENT_LISTENER,
13
13
  START_EVENT_LISTENER,
@@ -26,6 +26,7 @@ if TYPE_CHECKING:
26
26
  from port_ocean.core.integrations.base import BaseIntegration
27
27
  from port_ocean.ocean import Ocean
28
28
  from port_ocean.clients.port.client import PortClient
29
+ from port_ocean.core.handlers.actions.abstract_executor import AbstractExecutor
29
30
 
30
31
  from loguru import logger
31
32
 
@@ -73,9 +74,7 @@ class PortOceanContext:
73
74
  return self.app.port_client
74
75
 
75
76
  @property
76
- def event_listener_type(
77
- self,
78
- ) -> Literal["WEBHOOK", "KAFKA", "POLLING", "ONCE", "WEBHOOKS_ONLY"]:
77
+ def event_listener_type(self) -> EventListenerType:
79
78
  return self.app.config.event_listener.type
80
79
 
81
80
  def on_resync(
@@ -213,6 +212,9 @@ class PortOceanContext:
213
212
  """
214
213
  self.app.webhook_manager.register_processor(path, processor)
215
214
 
215
+ def register_action_executor(self, executor: "AbstractExecutor") -> None:
216
+ self.app.execution_manager.register_executor(executor)
217
+
216
218
 
217
219
  _port_ocean: PortOceanContext = PortOceanContext(None)
218
220
 
@@ -13,14 +13,16 @@ from port_ocean.core.defaults.common import (
13
13
  get_port_integration_defaults,
14
14
  )
15
15
  from port_ocean.core.handlers.port_app_config.models import PortAppConfig
16
- from port_ocean.core.models import Blueprint, CreatePortResourcesOrigin
16
+ from port_ocean.core.models import (
17
+ Blueprint,
18
+ CreatePortResourcesOrigin,
19
+ IntegrationFeatureFlag,
20
+ )
17
21
  from port_ocean.core.utils.utils import gather_and_split_errors_from_results
18
22
  from port_ocean.exceptions.port_defaults import (
19
23
  AbortDefaultCreationError,
20
24
  )
21
25
 
22
- ORG_USE_PROVISIONED_DEFAULTS_FEATURE_FLAG = "USE_PROVISIONED_DEFAULTS"
23
-
24
26
 
25
27
  def deconstruct_blueprints_to_creation_steps(
26
28
  raw_blueprints: list[dict[str, Any]],
@@ -75,6 +77,7 @@ async def _initialize_required_integration_settings(
75
77
  integration_config.integration.type,
76
78
  integration_config.event_listener.get_changelog_destination_details(),
77
79
  port_app_config=default_mapping,
80
+ actions_processing_enabled=integration_config.actions_processor.enabled,
78
81
  create_port_resources_origin_in_port=integration_config.create_port_resources_origin
79
82
  == CreatePortResourcesOrigin.Port,
80
83
  )
@@ -101,9 +104,13 @@ async def _initialize_required_integration_settings(
101
104
  integration.get("changelogDestination") != changelog_destination
102
105
  or integration.get("installationAppType") != integration_config.integration.type
103
106
  or integration.get("version") != port_client.integration_version
107
+ or integration.get("actionsProcessingEnabled")
108
+ != integration_config.actions_processor.enabled
104
109
  ):
105
110
  await port_client.patch_integration(
106
- integration_config.integration.type, changelog_destination
111
+ _type=integration_config.integration.type,
112
+ changelog_destination=changelog_destination,
113
+ actions_processing_enabled=integration_config.actions_processor.enabled,
107
114
  )
108
115
 
109
116
 
@@ -219,7 +226,7 @@ async def _initialize_defaults(
219
226
  )
220
227
  )
221
228
 
222
- has_provision_feature_flag = ORG_USE_PROVISIONED_DEFAULTS_FEATURE_FLAG in (
229
+ has_provision_feature_flag = IntegrationFeatureFlag.USE_PROVISIONED_DEFAULTS in (
223
230
  await port_client.get_organization_feature_flags()
224
231
  )
225
232
 
@@ -20,6 +20,10 @@ from port_ocean.core.event_listener.webhooks_only import (
20
20
  WebhooksOnlyEventListener,
21
21
  WebhooksOnlyEventListenerSettings,
22
22
  )
23
+ from port_ocean.core.event_listener.actions_only import (
24
+ ActionsOnlyEventListener,
25
+ ActionsOnlyEventListenerSettings,
26
+ )
23
27
 
24
28
 
25
29
  EventListenerSettingsType = (
@@ -28,6 +32,7 @@ EventListenerSettingsType = (
28
32
  | PollingEventListenerSettings
29
33
  | OnceEventListenerSettings
30
34
  | WebhooksOnlyEventListenerSettings
35
+ | ActionsOnlyEventListenerSettings
31
36
  )
32
37
 
33
38
  __all__ = [
@@ -42,4 +47,6 @@ __all__ = [
42
47
  "OnceEventListenerSettings",
43
48
  "WebhooksOnlyEventListener",
44
49
  "WebhooksOnlyEventListenerSettings",
50
+ "ActionsOnlyEventListener",
51
+ "ActionsOnlyEventListenerSettings",
45
52
  ]
@@ -0,0 +1,42 @@
1
+ from typing import Literal
2
+ from loguru import logger
3
+
4
+ from port_ocean.core.event_listener.base import (
5
+ BaseEventListener,
6
+ EventListenerEvents,
7
+ EventListenerSettings,
8
+ )
9
+ from port_ocean.core.models import EventListenerType
10
+
11
+
12
+ class ActionsOnlyEventListenerSettings(EventListenerSettings):
13
+ """
14
+ This class inherits from `EventListenerSettings`, which provides a foundation for creating event listener settings.
15
+ """
16
+
17
+ type: Literal[EventListenerType.ACTIONS_ONLY]
18
+ should_resync: bool = False
19
+ should_process_webhooks: bool = False
20
+
21
+
22
+ class ActionsOnlyEventListener(BaseEventListener):
23
+ """
24
+ No resync event listener.
25
+
26
+ It is used to handle events exclusively through actions without supporting resync events.
27
+
28
+ Parameters:
29
+ events (EventListenerEvents): A dictionary containing event types and their corresponding event handlers.
30
+ event_listener_config (ActionsOnlyEventListenerSettings): The event listener configuration settings.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ events: EventListenerEvents,
36
+ event_listener_config: ActionsOnlyEventListenerSettings,
37
+ ):
38
+ super().__init__(events)
39
+ self.event_listener_config = event_listener_config
40
+
41
+ async def _start(self) -> None:
42
+ logger.info("Starting Actions-only event listener")
@@ -4,6 +4,7 @@ from typing import TypedDict, Callable, Any, Awaitable
4
4
  from pydantic import Extra
5
5
 
6
6
  from port_ocean.config.base import BaseOceanModel
7
+ from port_ocean.core.models import EventListenerType
7
8
  from port_ocean.utils.signal import signal_handler
8
9
  from port_ocean.context.ocean import ocean
9
10
  from port_ocean.utils.misc import IntegrationStateStatus
@@ -78,8 +79,10 @@ class BaseEventListener:
78
79
 
79
80
 
80
81
  class EventListenerSettings(BaseOceanModel, extra=Extra.allow):
81
- type: str
82
+ type: EventListenerType
82
83
  should_resync: bool = True
84
+ should_process_webhooks: bool = True
85
+ should_run_actions: bool = True
83
86
 
84
87
  def get_changelog_destination_details(self) -> dict[str, Any]:
85
88
  """