port-ocean 0.5.17__tar.gz → 0.5.21__tar.gz
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.
- {port_ocean-0.5.17 → port_ocean-0.5.21}/PKG-INFO +1 -1
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +0 -1
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +1 -1
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/integrations.py +16 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/base.py +4 -6
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/settings.py +20 -6
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/base.py +0 -7
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/polling.py +2 -1
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entity_processor/base.py +6 -1
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entity_processor/jq_entity_processor.py +62 -25
- port_ocean-0.5.21/port_ocean/core/handlers/port_app_config/base.py +82 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/sync_raw.py +31 -5
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/utils.py +27 -31
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/sensetive.py +24 -9
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/ocean.py +10 -6
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/misc.py +15 -3
- port_ocean-0.5.21/port_ocean/utils/queue_utils.py +81 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/signal.py +1 -4
- {port_ocean-0.5.17 → port_ocean-0.5.21}/pyproject.toml +2 -2
- port_ocean-0.5.17/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -17
- port_ocean-0.5.17/port_ocean/core/handlers/port_app_config/base.py +0 -44
- {port_ocean-0.5.17 → port_ocean-0.5.21}/LICENSE.md +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/README.md +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/bootstrap.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cli.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/__init___.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/clean.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/dock.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/group.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/list_integrations.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/main.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/new.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/pull.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/sail.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/version.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/cookiecutter.json +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/extensions.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/hooks/post_gen_project.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/debug.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/poetry.toml +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/sonar-project.properties +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/utils.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/authentication.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/client.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/blueprints.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/entities.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/migrations.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/retry_transport.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/types.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/utils.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/dynamic.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/consumers/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/consumers/kafka_consumer.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/event.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/ocean.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/resource.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/clean.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/common.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/initialize.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/factory.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/http.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/kafka.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/once.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/base.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/base.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/port/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/port/applier.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entity_processor/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/port_app_config/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/port_app_config/api.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/port_app_config/models.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/base.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/events.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/handler.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/sync.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/utils.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/models.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/ocean_types.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/api.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/base.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/clients.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/context.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/core.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/port_defaults.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/utils.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/helpers/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/helpers/async_client.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/helpers/retry.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/handlers.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/logger_setup.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/middlewares.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/py.typed +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/run.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/sonar-project.properties +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/__init__.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/async_http.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/async_iterators.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/cache.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/repeat.py +0 -0
- {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/version.py +0 -0
|
@@ -6,6 +6,7 @@ from starlette import status
|
|
|
6
6
|
|
|
7
7
|
from port_ocean.clients.port.authentication import PortAuthentication
|
|
8
8
|
from port_ocean.clients.port.utils import handle_status_code
|
|
9
|
+
from port_ocean.log.sensetive import sensitive_log_filter
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from port_ocean.core.handlers.port_app_config.models import PortAppConfig
|
|
@@ -137,3 +138,18 @@ class IntegrationClientMixin:
|
|
|
137
138
|
)
|
|
138
139
|
handle_status_code(response)
|
|
139
140
|
logger.debug("Logs successfully ingested")
|
|
141
|
+
|
|
142
|
+
async def ingest_integration_kind_examples(
|
|
143
|
+
self, kind: str, data: list[dict[str, Any]], should_log: bool = True
|
|
144
|
+
):
|
|
145
|
+
logger.debug(f"Ingesting examples for kind: {kind}")
|
|
146
|
+
headers = await self.auth.headers()
|
|
147
|
+
response = await self.client.post(
|
|
148
|
+
f"{self.auth.api_url}/integration/{self.integration_identifier}/kinds/{kind}/examples",
|
|
149
|
+
headers=headers,
|
|
150
|
+
json={
|
|
151
|
+
"examples": sensitive_log_filter.mask_object(data, full_hide=True),
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
handle_status_code(response, should_log=should_log)
|
|
155
|
+
logger.debug(f"Examples for kind {kind} successfully ingested")
|
|
@@ -16,16 +16,15 @@ PROVIDER_CONFIG_PATTERN = r"^[a-zA-Z0-9]+ .*$"
|
|
|
16
16
|
|
|
17
17
|
def read_yaml_config_settings_source(
|
|
18
18
|
settings: "BaseOceanSettings", base_path: str
|
|
19
|
-
) -> str:
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
20
|
yaml_file = getattr(settings.__config__, "yaml_file", "")
|
|
21
21
|
|
|
22
22
|
assert yaml_file, "Settings.yaml_file not properly configured"
|
|
23
23
|
path = Path(base_path, yaml_file)
|
|
24
24
|
|
|
25
25
|
if not path.exists():
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return path.read_text("utf-8")
|
|
26
|
+
return {}
|
|
27
|
+
return yaml.safe_load(path.read_text("utf-8"))
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
def parse_config_provider(value: str) -> tuple[str, str]:
|
|
@@ -122,8 +121,7 @@ def decamelize_config(
|
|
|
122
121
|
def load_providers(
|
|
123
122
|
settings: "BaseOceanSettings", existing_values: dict[str, Any], base_path: str
|
|
124
123
|
) -> dict[str, Any]:
|
|
125
|
-
|
|
126
|
-
data = yaml.safe_load(yaml_content)
|
|
124
|
+
data = read_yaml_config_settings_source(settings, base_path)
|
|
127
125
|
snake_case_config = decamelize_config(settings, data)
|
|
128
126
|
return parse_providers(settings, snake_case_config, existing_values)
|
|
129
127
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from typing import Any, Literal
|
|
2
2
|
|
|
3
|
-
from pydantic import Extra, AnyHttpUrl, parse_obj_as
|
|
3
|
+
from pydantic import Extra, AnyHttpUrl, parse_obj_as
|
|
4
|
+
from pydantic.class_validators import root_validator
|
|
4
5
|
from pydantic.env_settings import InitSettingsSource, EnvSettingsSource, BaseSettings
|
|
5
6
|
from pydantic.fields import Field
|
|
6
7
|
from pydantic.main import BaseModel
|
|
7
8
|
|
|
8
9
|
from port_ocean.config.base import BaseOceanSettings, BaseOceanModel
|
|
9
10
|
from port_ocean.core.event_listener import EventListenerSettingsType
|
|
11
|
+
from port_ocean.utils.misc import get_integration_name
|
|
10
12
|
|
|
11
13
|
LogLevelType = Literal["ERROR", "WARNING", "INFO", "DEBUG", "CRITICAL"]
|
|
12
14
|
|
|
@@ -36,22 +38,34 @@ class PortSettings(BaseOceanModel, extra=Extra.allow):
|
|
|
36
38
|
client_id: str = Field(..., sensitive=True)
|
|
37
39
|
client_secret: str = Field(..., sensitive=True)
|
|
38
40
|
base_url: AnyHttpUrl = parse_obj_as(AnyHttpUrl, "https://api.getport.io")
|
|
41
|
+
port_app_config_cache_ttl: int = 60
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
class IntegrationSettings(BaseOceanModel, extra=Extra.allow):
|
|
42
|
-
identifier: str
|
|
43
|
-
type: str
|
|
45
|
+
identifier: str = Field(..., min_length=1)
|
|
46
|
+
type: str = Field(..., min_length=1)
|
|
44
47
|
config: dict[str, Any] | BaseModel
|
|
45
48
|
|
|
46
|
-
@
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
+
@root_validator(pre=True)
|
|
50
|
+
def a(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
integ_type = values.get("type")
|
|
52
|
+
|
|
53
|
+
if not integ_type:
|
|
54
|
+
integ_type = get_integration_name()
|
|
55
|
+
|
|
56
|
+
values["type"] = integ_type.lower() if integ_type else None
|
|
57
|
+
values["identifier"] = values.get(
|
|
58
|
+
"identifier", f"my-{integ_type}-integration".lower()
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return values
|
|
49
62
|
|
|
50
63
|
|
|
51
64
|
class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
|
|
52
65
|
initialize_port_resources: bool = True
|
|
53
66
|
scheduled_resync_interval: int | None = None
|
|
54
67
|
client_timeout: int = 30
|
|
68
|
+
send_raw_data_examples: bool = True
|
|
55
69
|
port: PortSettings
|
|
56
70
|
event_listener: EventListenerSettingsType
|
|
57
71
|
integration: IntegrationSettings
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from asyncio import Task
|
|
3
2
|
from typing import TypedDict, Callable, Any, Awaitable
|
|
4
3
|
|
|
5
4
|
from pydantic import Extra
|
|
@@ -22,7 +21,6 @@ class BaseEventListener:
|
|
|
22
21
|
events: EventListenerEvents,
|
|
23
22
|
):
|
|
24
23
|
self.events = events
|
|
25
|
-
self._tasks_to_close: list[Task[Any]] = []
|
|
26
24
|
|
|
27
25
|
async def start(self) -> None:
|
|
28
26
|
signal_handler.register(self._stop)
|
|
@@ -32,11 +30,6 @@ class BaseEventListener:
|
|
|
32
30
|
async def _start(self) -> None:
|
|
33
31
|
pass
|
|
34
32
|
|
|
35
|
-
def stop(self) -> None:
|
|
36
|
-
self._stop()
|
|
37
|
-
for task in self._tasks_to_close:
|
|
38
|
-
task.cancel()
|
|
39
|
-
|
|
40
33
|
def _stop(self) -> None:
|
|
41
34
|
"""
|
|
42
35
|
Can be used for event listeners that need cleanup before exiting.
|
|
@@ -10,6 +10,7 @@ from port_ocean.core.event_listener.base import (
|
|
|
10
10
|
EventListenerSettings,
|
|
11
11
|
)
|
|
12
12
|
from port_ocean.utils.repeat import repeat_every
|
|
13
|
+
from port_ocean.utils.signal import signal_handler
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class PollingEventListenerSettings(EventListenerSettings):
|
|
@@ -79,7 +80,7 @@ class PollingEventListener(BaseEventListener):
|
|
|
79
80
|
running_task: Task[Any] = get_event_loop().create_task(
|
|
80
81
|
self.events["on_resync"]({}) # type: ignore
|
|
81
82
|
)
|
|
82
|
-
|
|
83
|
+
signal_handler.register(running_task.cancel)
|
|
83
84
|
|
|
84
85
|
await running_task
|
|
85
86
|
|
|
@@ -40,6 +40,7 @@ class BaseEntityProcessor(BaseHandler):
|
|
|
40
40
|
mapping: ResourceConfig,
|
|
41
41
|
raw_data: list[RAW_ITEM],
|
|
42
42
|
parse_all: bool = False,
|
|
43
|
+
send_raw_data_examples_amount: int = 0,
|
|
43
44
|
) -> EntitySelectorDiff:
|
|
44
45
|
pass
|
|
45
46
|
|
|
@@ -48,6 +49,7 @@ class BaseEntityProcessor(BaseHandler):
|
|
|
48
49
|
mapping: ResourceConfig,
|
|
49
50
|
raw_data: list[RAW_ITEM],
|
|
50
51
|
parse_all: bool = False,
|
|
52
|
+
send_raw_data_examples_amount: int = 0,
|
|
51
53
|
) -> EntitySelectorDiff:
|
|
52
54
|
"""Public method to parse raw entity data and map it to an EntityDiff.
|
|
53
55
|
|
|
@@ -55,9 +57,12 @@ class BaseEntityProcessor(BaseHandler):
|
|
|
55
57
|
mapping (ResourceConfig): The configuration for entity mapping.
|
|
56
58
|
raw_data (list[RawEntity]): The raw data to be parsed.
|
|
57
59
|
parse_all (bool): Whether to parse all data or just data that passed the selector.
|
|
60
|
+
send_raw_data_examples_amount (bool): Whether to send example data to the integration service.
|
|
58
61
|
|
|
59
62
|
Returns:
|
|
60
63
|
EntityDiff: The parsed entity differences.
|
|
61
64
|
"""
|
|
62
65
|
with logger.contextualize(kind=mapping.kind):
|
|
63
|
-
return await self._parse_items(
|
|
66
|
+
return await self._parse_items(
|
|
67
|
+
mapping, raw_data, parse_all, send_raw_data_examples_amount
|
|
68
|
+
)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import functools
|
|
3
|
+
from dataclasses import dataclass, field
|
|
3
4
|
from functools import lru_cache
|
|
4
|
-
from typing import Any
|
|
5
|
-
from loguru import logger
|
|
5
|
+
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
import pyjq as jq # type: ignore
|
|
8
|
+
from loguru import logger
|
|
8
9
|
|
|
10
|
+
from port_ocean.context.ocean import ocean
|
|
9
11
|
from port_ocean.core.handlers.entity_processor.base import BaseEntityProcessor
|
|
10
12
|
from port_ocean.core.handlers.port_app_config.models import ResourceConfig
|
|
11
13
|
from port_ocean.core.models import Entity
|
|
@@ -14,6 +16,19 @@ from port_ocean.core.ocean_types import (
|
|
|
14
16
|
EntitySelectorDiff,
|
|
15
17
|
)
|
|
16
18
|
from port_ocean.exceptions.core import EntityProcessorException
|
|
19
|
+
from port_ocean.utils.queue_utils import process_in_queue
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class MappedEntity:
|
|
24
|
+
"""Represents the entity after applying the mapping
|
|
25
|
+
|
|
26
|
+
This class holds the mapping entity along with the selector boolean value and optionally the raw data.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
entity: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
did_entity_pass_selector: bool = False
|
|
31
|
+
raw_data: Optional[dict[str, Any]] = None
|
|
17
32
|
|
|
18
33
|
|
|
19
34
|
class JQEntityProcessor(BaseEntityProcessor):
|
|
@@ -77,13 +92,17 @@ class JQEntityProcessor(BaseEntityProcessor):
|
|
|
77
92
|
raw_entity_mappings: dict[str, Any],
|
|
78
93
|
selector_query: str,
|
|
79
94
|
parse_all: bool = False,
|
|
80
|
-
) ->
|
|
95
|
+
) -> MappedEntity:
|
|
81
96
|
should_run = await self._search_as_bool(data, selector_query)
|
|
82
97
|
if parse_all or should_run:
|
|
83
98
|
mapped_entity = await self._search_as_object(data, raw_entity_mappings)
|
|
84
|
-
return
|
|
99
|
+
return MappedEntity(
|
|
100
|
+
mapped_entity,
|
|
101
|
+
did_entity_pass_selector=should_run,
|
|
102
|
+
raw_data=data if should_run else None,
|
|
103
|
+
)
|
|
85
104
|
|
|
86
|
-
return
|
|
105
|
+
return MappedEntity()
|
|
87
106
|
|
|
88
107
|
async def _calculate_entity(
|
|
89
108
|
self,
|
|
@@ -92,7 +111,7 @@ class JQEntityProcessor(BaseEntityProcessor):
|
|
|
92
111
|
items_to_parse: str | None,
|
|
93
112
|
selector_query: str,
|
|
94
113
|
parse_all: bool = False,
|
|
95
|
-
) -> list[
|
|
114
|
+
) -> list[MappedEntity]:
|
|
96
115
|
if items_to_parse:
|
|
97
116
|
items = await self._search(data, items_to_parse)
|
|
98
117
|
if isinstance(items, list):
|
|
@@ -117,40 +136,58 @@ class JQEntityProcessor(BaseEntityProcessor):
|
|
|
117
136
|
data, raw_entity_mappings, selector_query, parse_all
|
|
118
137
|
)
|
|
119
138
|
]
|
|
120
|
-
return [(
|
|
139
|
+
return [MappedEntity()]
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
async def _send_examples(data: list[dict[str, Any]], kind: str) -> None:
|
|
143
|
+
try:
|
|
144
|
+
if data:
|
|
145
|
+
await ocean.port_client.ingest_integration_kind_examples(
|
|
146
|
+
kind, data, should_log=False
|
|
147
|
+
)
|
|
148
|
+
except Exception as ex:
|
|
149
|
+
logger.warning(
|
|
150
|
+
f"Failed to send raw data example {ex}",
|
|
151
|
+
exc_info=True,
|
|
152
|
+
)
|
|
121
153
|
|
|
122
154
|
async def _parse_items(
|
|
123
155
|
self,
|
|
124
156
|
mapping: ResourceConfig,
|
|
125
157
|
raw_results: list[RAW_ITEM],
|
|
126
158
|
parse_all: bool = False,
|
|
159
|
+
send_raw_data_examples_amount: int = 0,
|
|
127
160
|
) -> EntitySelectorDiff:
|
|
128
161
|
raw_entity_mappings: dict[str, Any] = mapping.port.entity.mappings.dict(
|
|
129
162
|
exclude_unset=True
|
|
130
163
|
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
for data in raw_results
|
|
142
|
-
]
|
|
143
|
-
calculate_entities_results = await asyncio.gather(*calculate_entities_tasks)
|
|
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,
|
|
172
|
+
)
|
|
144
173
|
|
|
145
174
|
passed_entities = []
|
|
146
175
|
failed_entities = []
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
176
|
+
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:
|
|
152
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)
|
|
153
188
|
else:
|
|
154
189
|
failed_entities.append(parsed_entity)
|
|
155
190
|
|
|
191
|
+
await self._send_examples(examples_to_send, mapping.kind)
|
|
192
|
+
|
|
156
193
|
return EntitySelectorDiff(passed=passed_entities, failed=failed_entities)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Type, Any
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from port_ocean.context.event import event
|
|
8
|
+
from port_ocean.context.ocean import PortOceanContext
|
|
9
|
+
from port_ocean.core.handlers.base import BaseHandler
|
|
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
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BasePortAppConfig(BaseHandler):
|
|
41
|
+
"""Abstract base class for managing port application configurations.
|
|
42
|
+
|
|
43
|
+
This class defines methods for obtaining and processing port application configurations.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
context (Any): The context to be used during port application configuration.
|
|
47
|
+
CONFIG_CLASS (Type[PortAppConfig]): The class used for defining port application configuration settings.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
CONFIG_CLASS: Type[PortAppConfig] = PortAppConfig
|
|
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
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def _get_port_app_config(self) -> dict[str, Any]:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
async def get_port_app_config(self, use_cache: bool = True) -> PortAppConfig:
|
|
63
|
+
"""
|
|
64
|
+
Retrieve and parse the port application configuration.
|
|
65
|
+
|
|
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.
|
|
68
|
+
"""
|
|
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
|
|
@@ -30,6 +30,9 @@ from port_ocean.core.utils import zip_and_sum
|
|
|
30
30
|
from port_ocean.exceptions.core import OceanAbortException
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
|
|
34
|
+
|
|
35
|
+
|
|
33
36
|
class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
34
37
|
"""Mixin class for synchronization of raw constructed entities.
|
|
35
38
|
|
|
@@ -124,10 +127,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
124
127
|
self,
|
|
125
128
|
raw_diff: list[tuple[ResourceConfig, list[RAW_ITEM]]],
|
|
126
129
|
parse_all: bool = False,
|
|
130
|
+
send_raw_data_examples_amount: int = 0,
|
|
127
131
|
) -> list[EntitySelectorDiff]:
|
|
128
132
|
return await asyncio.gather(
|
|
129
133
|
*(
|
|
130
|
-
self.entity_processor.parse_items(
|
|
134
|
+
self.entity_processor.parse_items(
|
|
135
|
+
mapping, results, parse_all, send_raw_data_examples_amount
|
|
136
|
+
)
|
|
131
137
|
for mapping, results in raw_diff
|
|
132
138
|
)
|
|
133
139
|
)
|
|
@@ -138,8 +144,11 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
138
144
|
results: list[dict[Any, Any]],
|
|
139
145
|
user_agent_type: UserAgentType,
|
|
140
146
|
parse_all: bool = False,
|
|
147
|
+
send_raw_data_examples_amount: int = 0,
|
|
141
148
|
) -> EntitySelectorDiff:
|
|
142
|
-
objects_diff = await self._calculate_raw(
|
|
149
|
+
objects_diff = await self._calculate_raw(
|
|
150
|
+
[(resource, results)], parse_all, send_raw_data_examples_amount
|
|
151
|
+
)
|
|
143
152
|
await self.entities_state_applier.upsert(
|
|
144
153
|
objects_diff[0].passed, user_agent_type
|
|
145
154
|
)
|
|
@@ -171,19 +180,32 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
171
180
|
else:
|
|
172
181
|
async_generators.append(result)
|
|
173
182
|
|
|
183
|
+
send_raw_data_examples_amount = (
|
|
184
|
+
SEND_RAW_DATA_EXAMPLES_AMOUNT if ocean.config.send_raw_data_examples else 0
|
|
185
|
+
)
|
|
174
186
|
entities = (
|
|
175
187
|
await self._register_resource_raw(
|
|
176
|
-
resource_config,
|
|
188
|
+
resource_config,
|
|
189
|
+
raw_results,
|
|
190
|
+
user_agent_type,
|
|
191
|
+
send_raw_data_examples_amount=send_raw_data_examples_amount,
|
|
177
192
|
)
|
|
178
193
|
).passed
|
|
179
194
|
|
|
180
195
|
for generator in async_generators:
|
|
181
196
|
try:
|
|
182
197
|
async for items in generator:
|
|
198
|
+
if send_raw_data_examples_amount > 0:
|
|
199
|
+
send_raw_data_examples_amount = max(
|
|
200
|
+
0, send_raw_data_examples_amount - len(entities)
|
|
201
|
+
)
|
|
183
202
|
entities.extend(
|
|
184
203
|
(
|
|
185
204
|
await self._register_resource_raw(
|
|
186
|
-
resource_config,
|
|
205
|
+
resource_config,
|
|
206
|
+
items,
|
|
207
|
+
user_agent_type,
|
|
208
|
+
send_raw_data_examples_amount=send_raw_data_examples_amount,
|
|
187
209
|
)
|
|
188
210
|
).passed
|
|
189
211
|
)
|
|
@@ -362,7 +384,11 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
362
384
|
EventType.RESYNC,
|
|
363
385
|
trigger_type=trigger_type,
|
|
364
386
|
):
|
|
365
|
-
|
|
387
|
+
# If a resync is triggered due to a mappings change, we want to make sure that we have the updated version
|
|
388
|
+
# rather than the old cache
|
|
389
|
+
app_config = await self.port_app_config_handler.get_port_app_config(
|
|
390
|
+
use_cache=False
|
|
391
|
+
)
|
|
366
392
|
|
|
367
393
|
creation_results: list[tuple[list[Entity], list[Exception]]] = []
|
|
368
394
|
|
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from typing import Callable, TYPE_CHECKING
|
|
2
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
5
|
from loguru import Record
|
|
@@ -35,16 +35,31 @@ class SensitiveLogFilter:
|
|
|
35
35
|
[re.compile(re.escape(token.strip())) for token in tokens if token.strip()]
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
def mask_string(self, string: str, full_hide: bool = False) -> str:
|
|
39
|
+
masked_string = string
|
|
40
|
+
for pattern in self.compiled_patterns:
|
|
41
|
+
replace: Callable[[re.Match[str]], str] | str = (
|
|
42
|
+
"[REDACTED]"
|
|
43
|
+
if full_hide
|
|
44
|
+
else lambda match: match.group()[:6] + "[REDACTED]"
|
|
45
|
+
)
|
|
46
|
+
masked_string = pattern.sub(replace, masked_string)
|
|
47
|
+
return masked_string
|
|
48
|
+
|
|
49
|
+
def mask_object(self, obj: Any, full_hide: bool = False) -> Any:
|
|
50
|
+
if isinstance(obj, str):
|
|
51
|
+
return self.mask_string(obj, full_hide)
|
|
52
|
+
if isinstance(obj, list):
|
|
53
|
+
return [self.mask_object(o, full_hide) for o in obj]
|
|
54
|
+
if isinstance(obj, dict):
|
|
55
|
+
for k, v in obj.items():
|
|
56
|
+
obj[k] = self.mask_object(v, full_hide)
|
|
57
|
+
|
|
58
|
+
return obj
|
|
59
|
+
|
|
38
60
|
def create_filter(self, full_hide: bool = False) -> Callable[["Record"], bool]:
|
|
39
61
|
def _filter(record: "Record") -> bool:
|
|
40
|
-
|
|
41
|
-
replace: Callable[[re.Match[str]], str] | str = (
|
|
42
|
-
"[REDACTED]"
|
|
43
|
-
if full_hide
|
|
44
|
-
else lambda match: match.group()[:6] + "[REDACTED]"
|
|
45
|
-
)
|
|
46
|
-
record["message"] = pattern.sub(replace, record["message"])
|
|
47
|
-
|
|
62
|
+
record["message"] = self.mask_string(record["message"], full_hide)
|
|
48
63
|
return True
|
|
49
64
|
|
|
50
65
|
return _filter
|