port-ocean 0.22.9__py3-none-any.whl → 0.22.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of port-ocean might be problematic. Click here for more details.

@@ -25,6 +25,10 @@ class LogAttributes(TypedDict):
25
25
  ingestUrl: str
26
26
 
27
27
 
28
+ class MetricsAttributes(TypedDict):
29
+ ingestUrl: str
30
+
31
+
28
32
  class IntegrationClientMixin:
29
33
  def __init__(
30
34
  self,
@@ -38,6 +42,7 @@ class IntegrationClientMixin:
38
42
  self.auth = auth
39
43
  self.client = client
40
44
  self._log_attributes: LogAttributes | None = None
45
+ self._metrics_attributes: MetricsAttributes | None = None
41
46
 
42
47
  async def is_integration_provision_enabled(
43
48
  self, integration_type: str, should_raise: bool = True, should_log: bool = True
@@ -106,6 +111,12 @@ class IntegrationClientMixin:
106
111
  self._log_attributes = response["logAttributes"]
107
112
  return self._log_attributes
108
113
 
114
+ async def get_metrics_attributes(self) -> LogAttributes:
115
+ if self._metrics_attributes is None:
116
+ response = await self.get_current_integration()
117
+ self._metrics_attributes = response["metricAttributes"]
118
+ return self._metrics_attributes
119
+
109
120
  async def _poll_integration_until_default_provisioning_is_complete(
110
121
  self,
111
122
  ) -> Dict[str, Any]:
@@ -197,6 +208,40 @@ class IntegrationClientMixin:
197
208
  handle_port_status_code(response)
198
209
  return response.json()["integration"]
199
210
 
211
+ async def post_integration_sync_metrics(
212
+ self, metrics: list[dict[str, Any]]
213
+ ) -> None:
214
+ logger.debug("starting POST metrics request", metrics=metrics)
215
+ metrics_attributes = await self.get_metrics_attributes()
216
+ headers = await self.auth.headers()
217
+ response = await self.client.post(
218
+ metrics_attributes["ingestUrl"],
219
+ headers=headers,
220
+ json={
221
+ "syncKindsMetrics": metrics,
222
+ },
223
+ )
224
+ handle_port_status_code(response, should_log=False)
225
+ logger.debug("Finished POST metrics request")
226
+
227
+ async def put_integration_sync_metrics(self, kind_metrics: dict[str, Any]) -> None:
228
+ logger.debug("starting PUT metrics request", kind_metrics=kind_metrics)
229
+ metrics_attributes = await self.get_metrics_attributes()
230
+ url = (
231
+ metrics_attributes["ingestUrl"]
232
+ + f"/resync/{kind_metrics['eventId']}/kind/{kind_metrics['kindIdentifier']}"
233
+ )
234
+ headers = await self.auth.headers()
235
+ response = await self.client.put(
236
+ url,
237
+ headers=headers,
238
+ json={
239
+ "syncKindMetrics": kind_metrics,
240
+ },
241
+ )
242
+ handle_port_status_code(response, should_log=False)
243
+ logger.debug("Finished PUT metrics request")
244
+
200
245
  async def ingest_integration_logs(self, logs: list[dict[str, Any]]) -> None:
201
246
  logger.debug("Ingesting logs")
202
247
  log_attributes = await self.get_log_attributes()
@@ -104,8 +104,12 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
104
104
  ):
105
105
  await self._safe_delete(diff.deleted, kept_entities, user_agent_type)
106
106
  ocean.metrics.inc_metric(
107
- name=MetricType.DELETION_COUNT_NAME,
108
- labels=[ocean.metrics.current_resource_kind(), MetricPhase.DELETE],
107
+ name=MetricType.OBJECT_COUNT_NAME,
108
+ labels=[
109
+ ocean.metrics.current_resource_kind(),
110
+ MetricPhase.DELETE,
111
+ MetricPhase.DeletionResult.DELETED,
112
+ ],
109
113
  value=len(diff.deleted),
110
114
  )
111
115
  else:
@@ -90,4 +90,10 @@ class ResyncStateUpdater:
90
90
  labels=[ocean.metrics.current_resource_kind(), MetricPhase.RESYNC],
91
91
  value=int(status == IntegrationStateStatus.Completed),
92
92
  )
93
- await ocean.metrics.flush(kind=ocean.metrics.current_resource_kind())
93
+
94
+ await ocean.metrics.send_metrics_to_webhook(
95
+ kind=ocean.metrics.current_resource_kind()
96
+ )
97
+ # await ocean.metrics.report_sync_metrics(
98
+ # kinds=[ocean.metrics.current_resource_kind()]
99
+ # ) # TODO: uncomment this when end points are ready
@@ -614,8 +614,10 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
614
614
  use_cache=False
615
615
  )
616
616
  logger.info(f"Resync will use the following mappings: {app_config.dict()}")
617
- ocean.metrics.initialize_metrics([f"{resource.kind}-{index}" for index, resource in enumerate(app_config.resources)])
618
- await ocean.metrics.flush()
617
+
618
+ kinds = [f"{resource.kind}-{index}" for index, resource in enumerate(app_config.resources)]
619
+ ocean.metrics.initialize_metrics(kinds)
620
+ # await ocean.metrics.report_sync_metrics(kinds=kinds) # TODO: uncomment this when end points are ready
619
621
 
620
622
  # Execute resync_start hooks
621
623
  for resync_start_fn in self.event_strategy["resync_start"]:
@@ -643,7 +645,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
643
645
  async with resource_context(resource,index):
644
646
  resource_kind_id = f"{resource.kind}-{index}"
645
647
  ocean.metrics.sync_state = SyncState.SYNCING
646
- await ocean.metrics.flush(kind=resource_kind_id)
648
+ # await ocean.metrics.report_kind_sync_metrics(kind=resource_kind_id) # TODO: uncomment this when end points are ready
647
649
 
648
650
  task = asyncio.create_task(
649
651
  self._register_in_batches(resource, user_agent_type)
@@ -653,9 +655,14 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
653
655
  kind_results: tuple[list[Entity], list[Exception]] = await task
654
656
 
655
657
  creation_results.append(kind_results)
658
+
656
659
  if ocean.metrics.sync_state != SyncState.FAILED:
657
660
  ocean.metrics.sync_state = SyncState.COMPLETED
658
- await ocean.metrics.flush(kind=resource_kind_id)
661
+
662
+ await ocean.metrics.send_metrics_to_webhook(
663
+ kind=resource_kind_id
664
+ )
665
+ # await ocean.metrics.report_kind_sync_metrics(kind=resource_kind_id) # TODO: uncomment this when end points are ready
659
666
 
660
667
  await self.sort_and_upsert_failed_entities(user_agent_type)
661
668
 
@@ -13,6 +13,7 @@ import prometheus_client.parser
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from port_ocean.config.settings import MetricsSettings, IntegrationSettings
16
+ from port_ocean.clients.port.client import PortClient
16
17
 
17
18
 
18
19
  class MetricPhase:
@@ -35,21 +36,22 @@ class MetricPhase:
35
36
  class ExtractResult:
36
37
  EXTRACTED = "raw_extracted"
37
38
 
39
+ class DeletionResult:
40
+ DELETED = "deleted"
41
+
38
42
 
39
43
  class MetricType:
40
44
  # Define metric names as constants
41
45
  DURATION_NAME = "duration_seconds"
42
46
  OBJECT_COUNT_NAME = "object_count"
43
- ERROR_COUNT_NAME = "error_count"
44
47
  SUCCESS_NAME = "success"
45
48
  RATE_LIMIT_WAIT_NAME = "rate_limit_wait_seconds"
46
- DELETION_COUNT_NAME = "deletion_count"
47
49
 
48
50
 
49
51
  class SyncState:
50
52
  SYNCING = "syncing"
51
53
  COMPLETED = "completed"
52
- QUEUED = "queued"
54
+ PENDING = "pending"
53
55
  FAILED = "failed"
54
56
 
55
57
 
@@ -65,11 +67,6 @@ _metrics_registry: Dict[str, Tuple[str, str, List[str]]] = {
65
67
  "object_count description",
66
68
  ["kind", "phase", "object_count_type"],
67
69
  ),
68
- MetricType.ERROR_COUNT_NAME: (
69
- MetricType.ERROR_COUNT_NAME,
70
- "error_count description",
71
- ["kind", "phase"],
72
- ),
73
70
  MetricType.SUCCESS_NAME: (
74
71
  MetricType.SUCCESS_NAME,
75
72
  "success description",
@@ -80,11 +77,6 @@ _metrics_registry: Dict[str, Tuple[str, str, List[str]]] = {
80
77
  "rate_limit_wait description",
81
78
  ["kind", "phase", "endpoint"],
82
79
  ),
83
- MetricType.DELETION_COUNT_NAME: (
84
- MetricType.DELETION_COUNT_NAME,
85
- "deletion_count description",
86
- ["kind", "phase"],
87
- ),
88
80
  }
89
81
 
90
82
 
@@ -115,16 +107,18 @@ class Metrics:
115
107
  self,
116
108
  metrics_settings: "MetricsSettings",
117
109
  integration_configuration: "IntegrationSettings",
110
+ port_client: "PortClient",
118
111
  ) -> None:
119
112
  self.metrics_settings = metrics_settings
120
113
  self.integration_configuration = integration_configuration
114
+ self.port_client = port_client
121
115
  self.registry = prometheus_client.CollectorRegistry()
122
116
  self.metrics: dict[str, Gauge] = {}
123
117
  self.load_metrics()
124
118
  self._integration_version: Optional[str] = None
125
119
  self._ocean_version: Optional[str] = None
126
120
  self.event_id = ""
127
- self.sync_state = SyncState.QUEUED
121
+ self.sync_state = SyncState.PENDING
128
122
 
129
123
  @property
130
124
  def event_id(self) -> str:
@@ -163,9 +157,6 @@ class Metrics:
163
157
  return self.metrics_settings.enabled
164
158
 
165
159
  def load_metrics(self) -> None:
166
- if not self.enabled:
167
- return None
168
-
169
160
  # Load all registered metrics
170
161
  for name, (_, description, labels) in _metrics_registry.items():
171
162
  self.metrics[name] = Gauge(
@@ -173,8 +164,6 @@ class Metrics:
173
164
  )
174
165
 
175
166
  def get_metric(self, name: str, labels: list[str]) -> Gauge | EmptyMetric:
176
- if not self.enabled:
177
- return EmptyMetric()
178
167
  metrics = self.metrics.get(name)
179
168
  if not metrics:
180
169
  return EmptyMetric()
@@ -188,9 +177,6 @@ class Metrics:
188
177
  labels (list[str]): The labels to apply to the metric.
189
178
  value (float): The value to inc.
190
179
  """
191
- if not self.enabled:
192
- return None
193
-
194
180
  self.get_metric(name, labels).inc(value)
195
181
 
196
182
  def set_metric(self, name: str, labels: list[str], value: float) -> None:
@@ -201,9 +187,6 @@ class Metrics:
201
187
  labels (list[str]): The labels to apply to the metric.
202
188
  value (float): The value to set.
203
189
  """
204
- if not self.enabled:
205
- return None
206
-
207
190
  self.get_metric(name, labels).set(value)
208
191
 
209
192
  def initialize_metrics(self, kind_blockes: list[str]) -> None:
@@ -271,15 +254,39 @@ class Metrics:
271
254
  self.registry
272
255
  ).decode()
273
256
 
274
- async def flush(
275
- self, metric_name: Optional[str] = None, kind: Optional[str] = None
257
+ async def report_sync_metrics(
258
+ self, metric_name: Optional[str] = None, kinds: Optional[list[str]] = None
276
259
  ) -> None:
277
- if not self.enabled:
260
+ if kinds is None:
278
261
  return None
279
262
 
280
- if not self.metrics_settings.webhook_url:
263
+ metrics = []
264
+
265
+ for kind in kinds:
266
+ metric = self.generate_metrics(metric_name, kind)
267
+ metrics.extend(metric)
268
+
269
+ try:
270
+ await self.port_client.post_integration_sync_metrics(metrics)
271
+ except Exception as e:
272
+ logger.error(f"Error posting metrics: {e}", metrics=metrics)
273
+
274
+ async def report_kind_sync_metrics(
275
+ self, metric_name: Optional[str] = None, kind: Optional[str] = None
276
+ ) -> None:
277
+ metrics = self.generate_metrics(metric_name, kind)
278
+ if not metrics:
281
279
  return None
282
280
 
281
+ try:
282
+ for metric in metrics:
283
+ await self.port_client.put_integration_sync_metrics(metric)
284
+ except Exception as e:
285
+ logger.error(f"Error putting metrics: {e}", metrics=metrics)
286
+
287
+ def generate_metrics(
288
+ self, metric_name: Optional[str] = None, kind: Optional[str] = None
289
+ ) -> list[dict[str, Any]]:
283
290
  try:
284
291
  latest_raw = self.generate_latest()
285
292
  metric_families = prometheus_client.parser.text_string_to_metric_families(
@@ -318,27 +325,54 @@ class Metrics:
318
325
 
319
326
  # If no metrics were filtered, exit early
320
327
  if not metrics_dict.get("kind", {}):
321
- return None
328
+ return []
322
329
 
330
+ events = []
323
331
  for kind_key, metrics in metrics_dict.get("kind", {}).items():
324
332
  # Skip if we're filtering by kind and this isn't the requested kind
325
333
  if kind and kind_key != kind:
326
334
  continue
327
335
 
328
336
  event = {
329
- "integration_type": self.integration_configuration.type,
330
- "integration_identifier": self.integration_configuration.identifier,
331
- "integration_version": self.integration_version,
332
- "ocean_version": self.ocean_version,
333
- "kind_identifier": kind_key,
334
- "kind": "-".join(kind_key.split("-")[:-1]),
335
- "event_id": self.event_id,
336
- "sync_state": self.sync_state,
337
+ "integrationType": self.integration_configuration.type,
338
+ "integrationIdentifier": self.integration_configuration.identifier,
339
+ "integrationVersion": self.integration_version,
340
+ "oceanVersion": self.ocean_version,
341
+ "kindIdentifier": kind_key,
342
+ "kind": (
343
+ "-".join(kind_key.split("-")[:-1])
344
+ if "-" in kind_key
345
+ else kind_key
346
+ ),
347
+ "kindIndex": 0 if kind_key == "__runtime__" else int(kind_key[-1]),
348
+ "eventId": self.event_id,
349
+ "syncState": self.sync_state,
337
350
  "metrics": metrics,
338
351
  }
339
- logger.info(f"Sending metrics to webhook {kind_key}: {event}")
352
+ events.append(event)
353
+ return events
354
+ except Exception as e:
355
+ logger.error(f"Error sending metrics to webhook: {e}")
356
+ return []
357
+
358
+ async def send_metrics_to_webhook(
359
+ self, metric_name: Optional[str] = None, kind: Optional[str] = None
360
+ ) -> None:
361
+ try:
362
+ if not self.enabled:
363
+ return None
364
+
365
+ if not self.metrics_settings.webhook_url:
366
+ return None
367
+
368
+ metrics = self.generate_metrics(metric_name, kind)
369
+ if not metrics:
370
+ return None
371
+
372
+ for metric in metrics:
373
+ logger.info(f"Sending metrics to webhook {metric['kind']}: {metric}")
340
374
  await AsyncClient().post(
341
- url=self.metrics_settings.webhook_url, json=event
375
+ url=self.metrics_settings.webhook_url, json=metric
342
376
  )
343
377
  except Exception as e:
344
378
  logger.error(f"Error sending metrics to webhook: {e}")
@@ -11,8 +11,6 @@ def TimeMetric(phase: str) -> Any:
11
11
 
12
12
  @wraps(func)
13
13
  async def wrapper(*args: Any, **kwargs: dict[Any, Any]) -> Any:
14
- if not ocean.metrics.enabled:
15
- return await func(*args, **kwargs)
16
14
  start = time.monotonic()
17
15
  res = await func(*args, **kwargs)
18
16
  end = time.monotonic()
port_ocean/ocean.py CHANGED
@@ -57,9 +57,20 @@ class Ocean:
57
57
  *self.config.get_sensitive_fields_data()
58
58
  )
59
59
  self.integration_router = integration_router or APIRouter()
60
+
61
+ self.port_client = PortClient(
62
+ base_url=self.config.port.base_url,
63
+ client_id=self.config.port.client_id,
64
+ client_secret=self.config.port.client_secret,
65
+ integration_identifier=self.config.integration.identifier,
66
+ integration_type=self.config.integration.type,
67
+ integration_version=__integration_version__,
68
+ )
69
+
60
70
  self.metrics = port_ocean.helpers.metric.metric.Metrics(
61
71
  metrics_settings=self.config.metrics,
62
72
  integration_configuration=self.config.integration,
73
+ port_client=self.port_client,
63
74
  )
64
75
 
65
76
  self.webhook_manager = LiveEventsProcessorManager(
@@ -69,14 +80,6 @@ class Ocean:
69
80
  max_wait_seconds_before_shutdown=self.config.max_wait_seconds_before_shutdown,
70
81
  )
71
82
 
72
- self.port_client = PortClient(
73
- base_url=self.config.port.base_url,
74
- client_id=self.config.port.client_id,
75
- client_secret=self.config.port.client_secret,
76
- integration_identifier=self.config.integration.identifier,
77
- integration_type=self.config.integration.type,
78
- integration_version=__integration_version__,
79
- )
80
83
  self.integration = (
81
84
  integration_class(ocean) if integration_class else BaseIntegration(ocean)
82
85
  )
@@ -1,6 +1,7 @@
1
1
  from graphlib import CycleError
2
- from typing import Any
2
+ from typing import Any, AsyncGenerator
3
3
 
4
+ from port_ocean.clients.port.client import PortClient
4
5
  from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
5
6
  from port_ocean.exceptions.core import OceanAbortException
6
7
  import pytest
@@ -24,6 +25,8 @@ from port_ocean.clients.port.types import UserAgentType
24
25
  from dataclasses import dataclass
25
26
  from typing import List, Optional
26
27
  from port_ocean.tests.core.conftest import create_entity, no_op_event_context
28
+ from port_ocean.helpers.metric.metric import Metrics
29
+ from port_ocean.config.settings import MetricsSettings, IntegrationSettings
27
30
 
28
31
 
29
32
  @pytest.fixture
@@ -36,7 +39,47 @@ def mock_sync_raw_mixin(
36
39
  sync_raw_mixin._entity_processor = mock_entity_processor
37
40
  sync_raw_mixin._entities_state_applier = mock_entities_state_applier
38
41
  sync_raw_mixin._port_app_config_handler = mock_port_app_config_handler
39
- sync_raw_mixin._get_resource_raw_results = AsyncMock(return_value=([{}], [])) # type: ignore
42
+
43
+ # Create raw data that matches the entity structure
44
+ raw_data = [
45
+ {
46
+ "id": "entity_1",
47
+ "name": "Entity 1",
48
+ "service": "entity_3",
49
+ "web_url": "https://example.com/entity1",
50
+ },
51
+ {
52
+ "id": "entity_2",
53
+ "name": "Entity 2",
54
+ "service": "entity_4",
55
+ "web_url": "https://example.com/entity2",
56
+ },
57
+ {
58
+ "id": "entity_3",
59
+ "name": "Entity 3",
60
+ "service": "",
61
+ "web_url": "https://example.com/entity3",
62
+ },
63
+ {
64
+ "id": "entity_4",
65
+ "name": "Entity 4",
66
+ "service": "entity_3",
67
+ "web_url": "https://example.com/entity4",
68
+ },
69
+ {
70
+ "id": "entity_5",
71
+ "name": "Entity 5",
72
+ "service": "entity_1",
73
+ "web_url": "https://example.com/entity5",
74
+ },
75
+ ]
76
+
77
+ # Create an async generator that yields the raw data
78
+ async def raw_results_generator() -> AsyncGenerator[list[dict[str, Any]], None]:
79
+ yield raw_data
80
+
81
+ # Return a list containing the async generator and an empty error list
82
+ sync_raw_mixin._get_resource_raw_results = AsyncMock(return_value=([raw_results_generator()], [])) # type: ignore
40
83
  sync_raw_mixin._entity_processor.parse_items = AsyncMock(return_value=MagicMock()) # type: ignore
41
84
 
42
85
  return sync_raw_mixin
@@ -51,9 +94,33 @@ def mock_sync_raw_mixin_with_jq_processor(
51
94
  return mock_sync_raw_mixin
52
95
 
53
96
 
97
+ @pytest.fixture
98
+ def mock_ocean(mock_port_client: PortClient) -> Ocean:
99
+ with patch("port_ocean.ocean.Ocean.__init__", return_value=None):
100
+ ocean_mock = Ocean(
101
+ MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
102
+ )
103
+ ocean_mock.config = MagicMock()
104
+ ocean_mock.config.port = MagicMock()
105
+ ocean_mock.config.port.port_app_config_cache_ttl = 60
106
+ ocean_mock.port_client = mock_port_client
107
+
108
+ # Create real metrics instance
109
+ metrics_settings = MetricsSettings(enabled=True)
110
+ integration_settings = IntegrationSettings(type="test", identifier="test")
111
+ ocean_mock.metrics = Metrics(
112
+ metrics_settings=metrics_settings,
113
+ integration_configuration=integration_settings,
114
+ port_client=mock_port_client,
115
+ )
116
+
117
+ return ocean_mock
118
+
119
+
54
120
  @pytest.mark.asyncio
55
121
  async def test_sync_raw_mixin_self_dependency(
56
122
  mock_sync_raw_mixin: SyncRawMixin,
123
+ mock_ocean: Ocean,
57
124
  ) -> None:
58
125
  entities_params = [
59
126
  ("entity_1", "service", {"service": "entity_1"}, True),
@@ -62,8 +129,14 @@ async def test_sync_raw_mixin_self_dependency(
62
129
  entities = [create_entity(*entity_param) for entity_param in entities_params]
63
130
 
64
131
  calc_result_mock = MagicMock()
65
- calc_result_mock.entity_selector_diff.passed = entities
66
- calc_result_mock.errors = []
132
+ calc_result_mock.entity_selector_diff = EntitySelectorDiff(
133
+ passed=entities, failed=[] # No failed entities in this test case
134
+ )
135
+ calc_result_mock.errors = [] # No errors in this test case
136
+ calc_result_mock.number_of_transformed_entities = len(
137
+ entities
138
+ ) # Add this to match real behavior
139
+ calc_result_mock.misonfigured_entity_keys = {} # Add this to match real behavior
67
140
 
68
141
  mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
69
142
 
@@ -105,6 +178,50 @@ async def test_sync_raw_mixin_self_dependency(
105
178
  for call in mock_order_by_entities_dependencies.call_args_list
106
179
  ] == [entity for entity in entities if entity.identifier == "entity_1"]
107
180
 
181
+ # Add assertions for actual metrics
182
+ metrics = mock_ocean.metrics.generate_metrics()
183
+ assert len(metrics) == 2
184
+
185
+ # Verify object counts
186
+ for metric in metrics:
187
+ if metric["kind"] == "project":
188
+ assert (
189
+ metric["metrics"]["phase"]["extract"]["object_count_type"][
190
+ "raw_extracted"
191
+ ]["object_count"]
192
+ == 5
193
+ )
194
+ assert (
195
+ metric["metrics"]["phase"]["transform"][
196
+ "object_count_type"
197
+ ]["transformed"]["object_count"]
198
+ == 2
199
+ )
200
+ assert (
201
+ metric["metrics"]["phase"]["transform"][
202
+ "object_count_type"
203
+ ]["filtered_out"]["object_count"]
204
+ == 3
205
+ )
206
+ assert (
207
+ metric["metrics"]["phase"]["load"]["object_count_type"][
208
+ "failed"
209
+ ]["object_count"]
210
+ == 1
211
+ )
212
+ assert (
213
+ metric["metrics"]["phase"]["load"]["object_count_type"][
214
+ "loaded"
215
+ ]["object_count"]
216
+ == 1
217
+ )
218
+
219
+ # Verify success
220
+ assert metric["metrics"]["phase"]["resync"]["success"] == 1
221
+
222
+ # Verify sync state
223
+ assert metric["syncState"] == "completed"
224
+
108
225
 
109
226
  @pytest.mark.asyncio
110
227
  async def test_sync_raw_mixin_circular_dependency(
@@ -117,8 +234,14 @@ async def test_sync_raw_mixin_circular_dependency(
117
234
  entities = [create_entity(*entity_param) for entity_param in entities_params]
118
235
 
119
236
  calc_result_mock = MagicMock()
120
- calc_result_mock.entity_selector_diff.passed = entities
121
- calc_result_mock.errors = []
237
+ calc_result_mock.entity_selector_diff = EntitySelectorDiff(
238
+ passed=entities, failed=[] # No failed entities in this test case
239
+ )
240
+ calc_result_mock.errors = [] # No errors in this test case
241
+ calc_result_mock.number_of_transformed_entities = len(
242
+ entities
243
+ ) # Add this to match real behavior
244
+ calc_result_mock.misonfigured_entity_keys = {} # Add this to match real behavior
122
245
 
123
246
  mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
124
247
 
@@ -183,11 +306,56 @@ async def test_sync_raw_mixin_circular_dependency(
183
306
  == 2
184
307
  )
185
308
 
309
+ # Add assertions for actual metrics
310
+ metrics = mock_ocean.metrics.generate_metrics()
311
+ assert len(metrics) == 2
312
+
313
+ # Verify object counts
314
+ for metric in metrics:
315
+ if metric["kind"] == "project":
316
+ assert (
317
+ metric["metrics"]["phase"]["extract"]["object_count_type"][
318
+ "raw_extracted"
319
+ ]["object_count"]
320
+ == 5
321
+ )
322
+ assert (
323
+ metric["metrics"]["phase"]["transform"][
324
+ "object_count_type"
325
+ ]["transformed"]["object_count"]
326
+ == 2
327
+ )
328
+ assert (
329
+ metric["metrics"]["phase"]["transform"][
330
+ "object_count_type"
331
+ ]["filtered_out"]["object_count"]
332
+ == 3
333
+ )
334
+ assert (
335
+ metric["metrics"]["phase"]["load"]["object_count_type"][
336
+ "failed"
337
+ ]["object_count"]
338
+ == 2
339
+ )
340
+ assert (
341
+ metric["metrics"]["phase"]["load"]["object_count_type"][
342
+ "loaded"
343
+ ]["object_count"]
344
+ == 0
345
+ )
346
+
347
+ # Verify success
348
+ assert metric["metrics"]["phase"]["resync"]["success"] == 1
349
+
350
+ # Verify sync state
351
+ assert metric["syncState"] == "completed"
352
+
186
353
 
187
354
  @pytest.mark.asyncio
188
355
  async def test_sync_raw_mixin_dependency(
189
356
  mock_sync_raw_mixin: SyncRawMixin, mock_ocean: Ocean
190
357
  ) -> None:
358
+ # Create entities with more realistic data
191
359
  entities_params = [
192
360
  ("entity_1", "service", {"service": "entity_3"}, True),
193
361
  ("entity_2", "service", {"service": "entity_4"}, True),
@@ -197,10 +365,18 @@ async def test_sync_raw_mixin_dependency(
197
365
  ]
198
366
  entities = [create_entity(*entity_param) for entity_param in entities_params]
199
367
 
368
+ # Create a more realistic CalculationResult mock
200
369
  calc_result_mock = MagicMock()
201
- calc_result_mock.entity_selector_diff.passed = entities
202
- calc_result_mock.errors = []
370
+ calc_result_mock.entity_selector_diff = EntitySelectorDiff(
371
+ passed=entities, failed=[] # No failed entities in this test case
372
+ )
373
+ calc_result_mock.errors = [] # No errors in this test case
374
+ calc_result_mock.number_of_transformed_entities = len(
375
+ entities
376
+ ) # Add this to match real behavior
377
+ calc_result_mock.misonfigured_entity_keys = {} # Add this to match real behavior
203
378
 
379
+ # Mock the parse_items method to return our realistic mock
204
380
  mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
205
381
 
206
382
  mock_order_by_entities_dependencies = MagicMock(
@@ -270,6 +446,44 @@ async def test_sync_raw_mixin_dependency(
270
446
  "entity_3-entity_1-entity_4-entity_5-entity_2",
271
447
  )
272
448
 
449
+ # Add assertions for actual metrics
450
+ metrics = mock_ocean.metrics.generate_metrics()
451
+ assert len(metrics) == 2
452
+
453
+ # Verify object counts
454
+ for metric in metrics:
455
+ if metric["kind"] == "project":
456
+ assert (
457
+ metric["metrics"]["phase"]["extract"]["object_count_type"][
458
+ "raw_extracted"
459
+ ]["object_count"]
460
+ == 5
461
+ )
462
+ assert (
463
+ metric["metrics"]["phase"]["transform"][
464
+ "object_count_type"
465
+ ]["transformed"]["object_count"]
466
+ == 5
467
+ )
468
+ assert (
469
+ metric["metrics"]["phase"]["transform"][
470
+ "object_count_type"
471
+ ]["filtered_out"]["object_count"]
472
+ == 0
473
+ )
474
+ assert (
475
+ metric["metrics"]["phase"]["load"]["object_count_type"][
476
+ "failed"
477
+ ]["object_count"]
478
+ == 5
479
+ )
480
+
481
+ # Verify success
482
+ assert metric["metrics"]["phase"]["resync"]["success"] == 1
483
+
484
+ # Verify sync state
485
+ assert metric["syncState"] == "completed"
486
+
273
487
 
274
488
  @pytest.mark.asyncio
275
489
  async def test_register_raw(
@@ -670,6 +884,7 @@ async def test_register_resource_raw_skip_event_type_http_request_upsert_called_
670
884
  async def test_on_resync_start_hooks_are_called(
671
885
  mock_sync_raw_mixin: SyncRawMixin,
672
886
  mock_port_app_config: PortAppConfig,
887
+ mock_ocean: Ocean,
673
888
  ) -> None:
674
889
  # Setup
675
890
  resync_start_called = False
@@ -679,7 +894,9 @@ async def test_on_resync_start_hooks_are_called(
679
894
  resync_start_called = True
680
895
 
681
896
  mock_sync_raw_mixin.on_resync_start(on_resync_start)
682
-
897
+ mock_ocean.metrics.report_sync_metrics = AsyncMock(return_value=None) # type: ignore
898
+ mock_ocean.metrics.report_kind_sync_metrics = AsyncMock(return_value=None) # type: ignore
899
+ mock_ocean.metrics.send_metrics_to_webhook = AsyncMock(return_value=None) # type: ignore
683
900
  # Execute
684
901
  async with event_context(EventType.RESYNC, trigger_type="machine") as event:
685
902
  event.port_app_config = mock_port_app_config
@@ -707,6 +924,9 @@ async def test_on_resync_complete_hooks_are_called_on_success(
707
924
 
708
925
  mock_sync_raw_mixin.on_resync_complete(on_resync_complete)
709
926
  mock_ocean.port_client.search_entities.return_value = [] # type: ignore
927
+ mock_ocean.metrics.report_sync_metrics = AsyncMock(return_value=None) # type: ignore
928
+ mock_ocean.metrics.report_kind_sync_metrics = AsyncMock(return_value=None) # type: ignore
929
+ mock_ocean.metrics.send_metrics_to_webhook = AsyncMock(return_value=None) # type: ignore
710
930
 
711
931
  # Execute
712
932
  async with event_context(EventType.RESYNC, trigger_type="machine") as event:
@@ -777,6 +997,9 @@ async def test_multiple_on_resync_start_on_resync_complete_hooks_called_in_order
777
997
  mock_sync_raw_mixin.on_resync_complete(on_resync_complete2)
778
998
  mock_ocean.port_client.search_entities.return_value = [] # type: ignore
779
999
 
1000
+ mock_ocean.metrics.report_sync_metrics = AsyncMock(return_value=None) # type: ignore
1001
+ mock_ocean.metrics.report_kind_sync_metrics = AsyncMock(return_value=None) # type: ignore
1002
+ mock_ocean.metrics.send_metrics_to_webhook = AsyncMock(return_value=None) # type: ignore
780
1003
  # Execute
781
1004
  async with event_context(EventType.RESYNC, trigger_type="machine") as event:
782
1005
  event.port_app_config = mock_port_app_config
@@ -798,6 +1021,7 @@ async def test_multiple_on_resync_start_on_resync_complete_hooks_called_in_order
798
1021
  async def test_on_resync_start_hook_error_prevents_resync(
799
1022
  mock_sync_raw_mixin: SyncRawMixin,
800
1023
  mock_port_app_config: PortAppConfig,
1024
+ mock_ocean: Ocean,
801
1025
  ) -> None:
802
1026
  # Setup
803
1027
  resync_complete_called = False
@@ -812,7 +1036,9 @@ async def test_on_resync_start_hook_error_prevents_resync(
812
1036
 
813
1037
  mock_sync_raw_mixin.on_resync_start(on_resync_start)
814
1038
  mock_sync_raw_mixin.on_resync_complete(on_resync_complete)
815
-
1039
+ mock_ocean.metrics.report_sync_metrics = AsyncMock(return_value=None) # type: ignore
1040
+ mock_ocean.metrics.report_kind_sync_metrics = AsyncMock(return_value=None) # type: ignore
1041
+ mock_ocean.metrics.send_metrics_to_webhook = AsyncMock(return_value=None) # type: ignore
816
1042
  original_get_resource_raw_results = mock_sync_raw_mixin._get_resource_raw_results
817
1043
 
818
1044
  async def track_resync(*args: Any, **kwargs: Any) -> Any:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.22.9
3
+ Version: 0.22.11
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
@@ -53,7 +53,7 @@ port_ocean/clients/port/client.py,sha256=dv0mxIOde6J-wFi1FXXZkoNPVHrZzY7RSMhNkDD
53
53
  port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  port_ocean/clients/port/mixins/blueprints.py,sha256=aMCG4zePsMSMjMLiGrU37h5z5_ElfMzTcTvqvOI5wXY,4683
55
55
  port_ocean/clients/port/mixins/entities.py,sha256=-1Gs74z_8eviWItHIpveQhKdA7gnjbqZ3STS4jgGONs,11668
56
- port_ocean/clients/port/mixins/integrations.py,sha256=tvuP8jxgbDyTwGfp8hd0-NXfnsMjZFJXDEDzXroctnY,9381
56
+ port_ocean/clients/port/mixins/integrations.py,sha256=QuPZ4MC8FtJ-3FKbm0CqJ5TOxtSyaQLR_rsJ-p7-y_Q,11115
57
57
  port_ocean/clients/port/mixins/migrations.py,sha256=vdL_A_NNUogvzujyaRLIoZEu5vmKDY2BxTjoGP94YzI,1467
58
58
  port_ocean/clients/port/mixins/organization.py,sha256=A2cP5V49KnjoAXxjmnm_XGth4ftPSU0qURNfnyUyS_Y,1041
59
59
  port_ocean/clients/port/retry_transport.py,sha256=PtIZOAZ6V-ncpVysRUsPOgt8Sf01QLnTKB5YeKBxkJk,1861
@@ -87,7 +87,7 @@ port_ocean/core/handlers/base.py,sha256=cTarblazu8yh8xz2FpB-dzDKuXxtoi143XJgPbV_
87
87
  port_ocean/core/handlers/entities_state_applier/__init__.py,sha256=kgLZDCeCEzi4r-0nzW9k78haOZNf6PX7mJOUr34A4c8,173
88
88
  port_ocean/core/handlers/entities_state_applier/base.py,sha256=5wHL0icfFAYRPqk8iV_wN49GdJ3aRUtO8tumSxBi4Wo,2268
89
89
  port_ocean/core/handlers/entities_state_applier/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
- port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=Glw-iSZvN0WaaQL3qfOSb9uEXcy88_mSOzifMLNhhOc,6254
90
+ port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=CcrKlFG0eAvUqWit_2SjDrYfTDmMlr4PPSiPBIw7qhM,6367
91
91
  port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py,sha256=1zncwCbE-Gej0xaWKlzZgoXxOBe9bgs_YxlZ8QW3NdI,1751
92
92
  port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=lyv6xKzhYfd6TioUgR3AVRSJqj7JpAaj1LxxU2xAqeo,1720
93
93
  port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybem9MthOs7p1Wawac87uSXz9U8,156
@@ -101,7 +101,7 @@ port_ocean/core/handlers/queue/__init__.py,sha256=1fICM0ZLATmmj6f7cdq_eV2kmw0_jy
101
101
  port_ocean/core/handlers/queue/abstract_queue.py,sha256=q_gpaWFFZHxM3XovEbgsDn8jEOLM45iAZWVC81Paxto,620
102
102
  port_ocean/core/handlers/queue/local_queue.py,sha256=EzqsGIX43xbVAcePwTcCg5QDrXATQpy-VzWxxN_OyAM,574
103
103
  port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
104
- port_ocean/core/handlers/resync_state_updater/updater.py,sha256=K3E7MfO7SDO6SYRb_MPb4Cu3LTEumfYLHnhYV8IE0kY,3606
104
+ port_ocean/core/handlers/resync_state_updater/updater.py,sha256=KZ55dubVYmFHxUOzdUFBPGLQhQSVBiRAoyk3R-NKKRU,3819
105
105
  port_ocean/core/handlers/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
106
  port_ocean/core/handlers/webhook/abstract_webhook_processor.py,sha256=5KwZkdkDd5HdVkXPzKiqabodZKl-hOtMypkTKd8Hq3M,3891
107
107
  port_ocean/core/handlers/webhook/processor_manager.py,sha256=ipyAXoFtF84EGczyzRcZCzQG4Ippjo4eMsnVxMVz12A,12072
@@ -113,7 +113,7 @@ port_ocean/core/integrations/mixins/events.py,sha256=2L7P3Jhp8XBqddh2_o9Cn4N261n
113
113
  port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
114
114
  port_ocean/core/integrations/mixins/live_events.py,sha256=8HklZmlyffYY_LeDe8xbt3Tb08rlLkqVhFF-2NQeJP4,4126
115
115
  port_ocean/core/integrations/mixins/sync.py,sha256=Vm_898pLKBwfVewtwouDWsXoxcOLicnAy6pzyqqk6U8,4053
116
- port_ocean/core/integrations/mixins/sync_raw.py,sha256=ezjfpxZJ-0IjxppP6nKu4lSWjsh_8HPNghSgsQRayR8,29100
116
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=4ikStO_BurcB5Hy1uYcWMO-exYJbOnu6by9eYmMJbJU,29490
117
117
  port_ocean/core/integrations/mixins/utils.py,sha256=oN4Okz6xlaefpid1_Pud8HPSw9BwwjRohyNsknq-Myg,2309
118
118
  port_ocean/core/models.py,sha256=YpJ2XOB3Zt9_M-rcMrMjugFNzBDg2hCUKgqvEt7now0,2348
119
119
  port_ocean/core/ocean_types.py,sha256=4VipWFOHEh_d9LmWewQccwx1p2dtrRYW0YURVgNsAjo,1398
@@ -131,15 +131,15 @@ port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnq
131
131
  port_ocean/exceptions/webhook_processor.py,sha256=yQYazg53Y-ohb7HfViwq1opH_ZUuUdhHSRxcUNveFpI,114
132
132
  port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
133
  port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861vE3K40,1783
134
- port_ocean/helpers/metric/metric.py,sha256=Eg8t7k3hDumnUHe2TvE5xus3cWoROwJPjoviW4QrGoE,11440
135
- port_ocean/helpers/metric/utils.py,sha256=_qj9-PfsrMHoXVscEsIdnylLVZJnupg8ofHYaKc5-t8,874
134
+ port_ocean/helpers/metric/metric.py,sha256=F7R5JGmyGPsvqBY7wlDfOc15GX28-Kslbw99Y0aETv0,12785
135
+ port_ocean/helpers/metric/utils.py,sha256=Wnr-6HwVwBtYJ3so44OkhDRs8udLMSB1oduzl2-zRHo,781
136
136
  port_ocean/helpers/retry.py,sha256=gmS4YxM6N4fboFp7GSgtOzyBJemxs46bnrz4L4rDS6Y,16136
137
137
  port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
138
  port_ocean/log/handlers.py,sha256=ncVjgqrZRh6BhyRrA6DQG86Wsbxph1yWYuEC0cWfe-Q,3631
139
139
  port_ocean/log/logger_setup.py,sha256=0K3zVG0YYrYOWEV8-rCGks1o-bMRxgHXlqawu9w_tSw,2656
140
140
  port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2920
141
141
  port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
142
- port_ocean/ocean.py,sha256=XmPmnjo-2oMZGvCLwIxPxwVNzXXzACzVR4JmlkXEOKY,7418
142
+ port_ocean/ocean.py,sha256=8yUw9pDBGkqBHza_PKtINz_7G69Ia6o1m8H-SvlKOOk,7462
143
143
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
144
144
  port_ocean/run.py,sha256=COoRSmLG4hbsjIW5DzhV0NYVegI9xHd1POv6sg4U1No,2217
145
145
  port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
@@ -155,7 +155,7 @@ port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcG
155
155
  port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=WNg1fWZsXu0MDnz9-ahRiPb_OPofWx7E8wxBx0cyZKs,8946
156
156
  port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=8WpMn559Mf0TFWmloRpZrVgr6yWwyA0C4n2lVHCtyq4,13596
157
157
  port_ocean/tests/core/handlers/mixins/test_live_events.py,sha256=iAwVpr3n3PIkXQLw7hxd-iB_SR_vyfletVXJLOmyz28,12480
158
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=RTkHU2QS9f-Lt3u0OCBPPEeiaM2_9h5vOQxqLnrbQro,33560
158
+ port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=ZjAWXpheHa61M9nIj4FUGKt9xMeI4Z1AvE6Nko-uru8,43482
159
159
  port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
160
160
  port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=hSh556bJM9zuELwhwnyKSfd9z06WqWXIfe-6hCl5iKI,9799
161
161
  port_ocean/tests/core/handlers/queue/test_local_queue.py,sha256=9Ly0HzZXbs6Rbl_bstsIdInC3h2bgABU3roP9S_PnJM,2582
@@ -188,8 +188,8 @@ port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,32
188
188
  port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
189
189
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
190
190
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
191
- port_ocean-0.22.9.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
192
- port_ocean-0.22.9.dist-info/METADATA,sha256=H6ErJ-lhawBgcqSnlgMOEgOz-Nsc5GAh-p0spV6VExY,6764
193
- port_ocean-0.22.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
194
- port_ocean-0.22.9.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
195
- port_ocean-0.22.9.dist-info/RECORD,,
191
+ port_ocean-0.22.11.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
192
+ port_ocean-0.22.11.dist-info/METADATA,sha256=Fo942o0gljfDJTPxcKfgK3fpbBVSQluymPor_ztGo2Q,6765
193
+ port_ocean-0.22.11.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
194
+ port_ocean-0.22.11.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
195
+ port_ocean-0.22.11.dist-info/RECORD,,