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.

Files changed (130) hide show
  1. {port_ocean-0.5.17 → port_ocean-0.5.21}/PKG-INFO +1 -1
  2. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +0 -1
  3. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +1 -1
  4. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/integrations.py +16 -0
  5. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/base.py +4 -6
  6. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/settings.py +20 -6
  7. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/base.py +0 -7
  8. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/polling.py +2 -1
  9. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entity_processor/base.py +6 -1
  10. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entity_processor/jq_entity_processor.py +62 -25
  11. port_ocean-0.5.21/port_ocean/core/handlers/port_app_config/base.py +82 -0
  12. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/sync_raw.py +31 -5
  13. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/utils.py +27 -31
  14. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/sensetive.py +24 -9
  15. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/ocean.py +10 -6
  16. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/misc.py +15 -3
  17. port_ocean-0.5.21/port_ocean/utils/queue_utils.py +81 -0
  18. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/signal.py +1 -4
  19. {port_ocean-0.5.17 → port_ocean-0.5.21}/pyproject.toml +2 -2
  20. port_ocean-0.5.17/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -17
  21. port_ocean-0.5.17/port_ocean/core/handlers/port_app_config/base.py +0 -44
  22. {port_ocean-0.5.17 → port_ocean-0.5.21}/LICENSE.md +0 -0
  23. {port_ocean-0.5.17 → port_ocean-0.5.21}/README.md +0 -0
  24. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/__init__.py +0 -0
  25. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/bootstrap.py +0 -0
  26. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/__init__.py +0 -0
  27. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cli.py +0 -0
  28. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/__init__.py +0 -0
  29. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/__init___.py +0 -0
  30. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/clean.py +0 -0
  31. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/dock.py +0 -0
  32. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/defaults/group.py +0 -0
  33. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/list_integrations.py +0 -0
  34. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/main.py +0 -0
  35. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/new.py +0 -0
  36. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/pull.py +0 -0
  37. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/sail.py +0 -0
  38. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/commands/version.py +0 -0
  39. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/__init__.py +0 -0
  40. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/cookiecutter.json +0 -0
  41. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/extensions.py +0 -0
  42. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/hooks/post_gen_project.py +0 -0
  43. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -0
  44. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore +0 -0
  45. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore +0 -0
  46. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +0 -0
  47. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -0
  48. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile +0 -0
  49. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md +0 -0
  50. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +0 -0
  51. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/debug.py +0 -0
  52. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +0 -0
  53. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/poetry.toml +0 -0
  54. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/sonar-project.properties +0 -0
  55. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py +0 -0
  56. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/cli/utils.py +0 -0
  57. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/__init__.py +0 -0
  58. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/__init__.py +0 -0
  59. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/authentication.py +0 -0
  60. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/client.py +0 -0
  61. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/__init__.py +0 -0
  62. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/blueprints.py +0 -0
  63. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/entities.py +0 -0
  64. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/mixins/migrations.py +0 -0
  65. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/retry_transport.py +0 -0
  66. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/types.py +0 -0
  67. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/clients/port/utils.py +0 -0
  68. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/__init__.py +0 -0
  69. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/config/dynamic.py +0 -0
  70. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/consumers/__init__.py +0 -0
  71. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/consumers/kafka_consumer.py +0 -0
  72. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/__init__.py +0 -0
  73. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/event.py +0 -0
  74. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/ocean.py +0 -0
  75. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/context/resource.py +0 -0
  76. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/__init__.py +0 -0
  77. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/__init__.py +0 -0
  78. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/clean.py +0 -0
  79. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/common.py +0 -0
  80. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/defaults/initialize.py +0 -0
  81. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/__init__.py +0 -0
  82. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/factory.py +0 -0
  83. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/http.py +0 -0
  84. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/kafka.py +0 -0
  85. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/event_listener/once.py +0 -0
  86. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/__init__.py +0 -0
  87. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/base.py +0 -0
  88. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/__init__.py +0 -0
  89. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/base.py +0 -0
  90. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/port/__init__.py +0 -0
  91. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/port/applier.py +0 -0
  92. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py +0 -0
  93. {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
  94. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/entity_processor/__init__.py +0 -0
  95. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/port_app_config/__init__.py +0 -0
  96. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/port_app_config/api.py +0 -0
  97. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/handlers/port_app_config/models.py +0 -0
  98. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/__init__.py +0 -0
  99. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/base.py +0 -0
  100. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/__init__.py +0 -0
  101. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/events.py +0 -0
  102. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/handler.py +0 -0
  103. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/sync.py +0 -0
  104. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/integrations/mixins/utils.py +0 -0
  105. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/models.py +0 -0
  106. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/core/ocean_types.py +0 -0
  107. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/__init__.py +0 -0
  108. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/api.py +0 -0
  109. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/base.py +0 -0
  110. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/clients.py +0 -0
  111. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/context.py +0 -0
  112. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/core.py +0 -0
  113. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/port_defaults.py +0 -0
  114. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/exceptions/utils.py +0 -0
  115. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/helpers/__init__.py +0 -0
  116. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/helpers/async_client.py +0 -0
  117. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/helpers/retry.py +0 -0
  118. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/__init__.py +0 -0
  119. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/handlers.py +0 -0
  120. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/log/logger_setup.py +0 -0
  121. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/middlewares.py +0 -0
  122. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/py.typed +0 -0
  123. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/run.py +0 -0
  124. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/sonar-project.properties +0 -0
  125. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/__init__.py +0 -0
  126. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/async_http.py +0 -0
  127. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/async_iterators.py +0 -0
  128. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/cache.py +0 -0
  129. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/utils/repeat.py +0 -0
  130. {port_ocean-0.5.17 → port_ocean-0.5.21}/port_ocean/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.5.17
3
+ Version: 0.5.21
4
4
  Summary: Port Ocean is a CLI tool for managing your Port projects.
5
5
  Home-page: https://app.getport.io
6
6
  Keywords: ocean,port-ocean,port
@@ -1,4 +1,3 @@
1
- type: {{cookiecutter.integration_slug}}
2
1
  description: {{cookiecutter.integration_name}} integration for Port Ocean
3
2
  icon: Cookiecutter # Should be one of the available icons in Port
4
3
  features:
@@ -1,5 +1,5 @@
1
1
  [tool.poetry]
2
- name = "{{cookiecutter.integration_name}}"
2
+ name = "{{cookiecutter.integration_slug}}"
3
3
  version = "0.1.0"
4
4
  description = "{{cookiecutter.integration_short_description}}"
5
5
  authors = ["{{cookiecutter.full_name}} <{{cookiecutter.email}}>"]
@@ -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
- raise FileNotFoundError(f"Could not open yaml settings file at: {path}")
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
- yaml_content = read_yaml_config_settings_source(settings, base_path)
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, validator
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
- @validator("identifier", "type")
47
- def validate_lower(cls, v: str) -> str:
48
- return v.lower()
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
- self._tasks_to_close.append(running_task)
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(mapping, raw_data, parse_all)
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
- ) -> tuple[dict[str, Any], bool]:
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 mapped_entity, should_run
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 {}, False
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[tuple[dict[str, Any], bool]]:
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 [({}, False)]
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
- calculate_entities_tasks = [
132
- asyncio.create_task(
133
- self._calculate_entity(
134
- data,
135
- raw_entity_mappings,
136
- mapping.port.items_to_parse,
137
- mapping.selector.query,
138
- parse_all,
139
- )
140
- )
141
- for data in raw_results
142
- ]
143
- calculate_entities_results = await asyncio.gather(*calculate_entities_tasks)
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
- for entities_results in calculate_entities_results:
148
- for entity, did_entity_pass_selector in entities_results:
149
- if entity.get("identifier") and entity.get("blueprint"):
150
- parsed_entity = Entity.parse_obj(entity)
151
- if did_entity_pass_selector:
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(mapping, results, parse_all)
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([(resource, results)], parse_all)
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, raw_results, user_agent_type
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, items, user_agent_type
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
- app_config = await self.port_app_config_handler.get_port_app_config()
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
- return EntityPortDiff(
44
- deleted=get_unique(
45
- [
46
- item
47
- for item in before
48
- if not any(is_same_entity(item, item_after) for item_after in after)
49
- ],
50
- ),
51
- created=get_unique(
52
- [
53
- item
54
- for item in after
55
- if not any(is_same_entity(item, item_before) for item_before in before)
56
- ],
57
- ),
58
- modified=get_unique(
59
- [
60
- item
61
- for item in after
62
- if any(is_same_entity(item, item_before) for item_before in before)
63
- ],
64
- ),
65
- )
35
+ before_dict = {}
36
+ after_dict = {}
37
+ created = []
38
+ modified = []
39
+ deleted = []
40
+
41
+ # Create dictionaries for before and after lists
42
+ for entity in before:
43
+ key = (entity.identifier, entity.blueprint)
44
+ before_dict[key] = entity
45
+
46
+ for entity in after:
47
+ key = (entity.identifier, entity.blueprint)
48
+ after_dict[key] = entity
49
+
50
+ # Find created, modified, and deleted objects
51
+ for key, obj in after_dict.items():
52
+ if key not in before_dict:
53
+ created.append(obj)
54
+ else:
55
+ modified.append(obj)
56
+
57
+ for key, obj in before_dict.items():
58
+ if key not in after_dict:
59
+ deleted.append(obj)
60
+
61
+ return EntityPortDiff(created=created, modified=modified, deleted=deleted)
@@ -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
- 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
- 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