port-ocean 0.28.2__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.
- integrations/_infra/Dockerfile.Deb +6 -1
- integrations/_infra/Dockerfile.local +1 -0
- port_ocean/clients/port/authentication.py +19 -0
- port_ocean/clients/port/client.py +3 -0
- port_ocean/clients/port/mixins/actions.py +93 -0
- port_ocean/clients/port/mixins/blueprints.py +0 -12
- port_ocean/clients/port/mixins/entities.py +79 -44
- port_ocean/clients/port/mixins/integrations.py +7 -2
- port_ocean/config/settings.py +35 -3
- port_ocean/context/ocean.py +7 -5
- port_ocean/core/defaults/initialize.py +12 -5
- port_ocean/core/event_listener/__init__.py +7 -0
- port_ocean/core/event_listener/actions_only.py +42 -0
- port_ocean/core/event_listener/base.py +4 -1
- port_ocean/core/event_listener/factory.py +18 -9
- port_ocean/core/event_listener/http.py +4 -3
- port_ocean/core/event_listener/kafka.py +3 -2
- port_ocean/core/event_listener/once.py +5 -2
- port_ocean/core/event_listener/polling.py +4 -3
- port_ocean/core/event_listener/webhooks_only.py +3 -2
- port_ocean/core/handlers/actions/__init__.py +7 -0
- port_ocean/core/handlers/actions/abstract_executor.py +150 -0
- port_ocean/core/handlers/actions/execution_manager.py +434 -0
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py +479 -17
- port_ocean/core/handlers/entity_processor/jq_input_evaluator.py +137 -0
- port_ocean/core/handlers/port_app_config/models.py +4 -2
- port_ocean/core/handlers/resync_state_updater/updater.py +4 -2
- port_ocean/core/handlers/webhook/abstract_webhook_processor.py +16 -0
- port_ocean/core/handlers/webhook/processor_manager.py +30 -12
- port_ocean/core/integrations/mixins/sync_raw.py +10 -5
- port_ocean/core/integrations/mixins/utils.py +250 -29
- port_ocean/core/models.py +35 -2
- port_ocean/core/utils/utils.py +16 -5
- port_ocean/exceptions/execution_manager.py +22 -0
- port_ocean/helpers/metric/metric.py +1 -1
- port_ocean/helpers/retry.py +4 -40
- port_ocean/log/logger_setup.py +2 -2
- port_ocean/ocean.py +31 -5
- port_ocean/tests/clients/port/mixins/test_entities.py +71 -5
- port_ocean/tests/core/event_listener/test_kafka.py +14 -7
- port_ocean/tests/core/handlers/actions/test_execution_manager.py +837 -0
- port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +932 -1
- port_ocean/tests/core/handlers/entity_processor/test_jq_input_evaluator.py +932 -0
- port_ocean/tests/core/handlers/webhook/test_processor_manager.py +3 -1
- port_ocean/tests/core/utils/test_get_port_diff.py +164 -0
- port_ocean/tests/helpers/test_retry.py +241 -1
- port_ocean/tests/utils/test_cache.py +240 -0
- port_ocean/utils/cache.py +45 -9
- {port_ocean-0.28.2.dist-info → port_ocean-0.29.0.dist-info}/METADATA +2 -1
- {port_ocean-0.28.2.dist-info → port_ocean-0.29.0.dist-info}/RECORD +53 -43
- {port_ocean-0.28.2.dist-info → port_ocean-0.29.0.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.28.2.dist-info → port_ocean-0.29.0.dist-info}/WHEEL +0 -0
- {port_ocean-0.28.2.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}
|
|
@@ -69,18 +70,22 @@ COPY --from=base /app/.venv /app/.venv
|
|
|
69
70
|
COPY ./integrations/_infra/init.sh /app/init.sh
|
|
70
71
|
|
|
71
72
|
USER root
|
|
73
|
+
|
|
72
74
|
# Ensure that ocean is available for all in path
|
|
73
75
|
RUN chmod a+x /app/.venv/bin/ocean
|
|
74
76
|
|
|
75
77
|
RUN chmod a+x /app/init.sh
|
|
76
78
|
RUN ln -s /app/.venv/bin/ocean /usr/bin/ocean
|
|
79
|
+
|
|
77
80
|
# Add ocean user to ssl certs group
|
|
78
|
-
RUN setfacl -m u:ocean:rwX /etc/ssl/certs
|
|
81
|
+
RUN setfacl -R -m u:ocean:rwX /etc/ssl/certs \
|
|
82
|
+
&& setfacl -d -m u:ocean:rwX /etc/ssl/certs
|
|
79
83
|
|
|
80
84
|
# Allow ocean user to run update-ca-certificates without password (secure, limited sudo)
|
|
81
85
|
RUN echo "ocean ALL=(root) NOPASSWD: /usr/sbin/update-ca-certificates" >> /etc/sudoers.d/ocean-certs \
|
|
82
86
|
&& chmod 440 /etc/sudoers.d/ocean-certs
|
|
83
87
|
|
|
84
88
|
USER ocean
|
|
89
|
+
|
|
85
90
|
# Run the application
|
|
86
91
|
CMD ["bash", "/app/init.sh"]
|
|
@@ -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
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
"
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
522
|
-
query["rules"].extend(default_query["rules"])
|
|
532
|
+
return aggregated_entities
|
|
523
533
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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)
|
port_ocean/config/settings.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import platform
|
|
2
|
-
from typing import Any, Literal, Optional, Type
|
|
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
|
|
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
|
-
|
|
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
|
port_ocean/context/ocean.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Callable, TYPE_CHECKING, Any,
|
|
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
|
|
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,
|
|
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 =
|
|
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")
|