port-ocean 0.23.4__py3-none-any.whl → 0.24.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of port-ocean might be problematic. Click here for more details.
- integrations/_infra/Dockerfile.Deb +6 -6
- port_ocean/clients/port/mixins/entities.py +289 -28
- port_ocean/config/dynamic.py +1 -1
- port_ocean/config/settings.py +4 -0
- port_ocean/core/handlers/entities_state_applier/port/applier.py +1 -1
- port_ocean/core/integrations/mixins/sync_raw.py +2 -3
- port_ocean/core/models.py +20 -1
- port_ocean/exceptions/core.py +4 -0
- port_ocean/helpers/metric/metric.py +3 -1
- port_ocean/tests/clients/port/mixins/test_entities.py +132 -14
- port_ocean/tests/core/conftest.py +50 -15
- port_ocean/tests/core/handlers/entities_state_applier/test_applier.py +41 -69
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +32 -10
- port_ocean/tests/log/test_handlers.py +2 -1
- {port_ocean-0.23.4.dist-info → port_ocean-0.24.0.dist-info}/METADATA +1 -1
- {port_ocean-0.23.4.dist-info → port_ocean-0.24.0.dist-info}/RECORD +19 -19
- {port_ocean-0.23.4.dist-info → port_ocean-0.24.0.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.23.4.dist-info → port_ocean-0.24.0.dist-info}/WHEEL +0 -0
- {port_ocean-0.23.4.dist-info → port_ocean-0.24.0.dist-info}/entry_points.txt +0 -0
|
@@ -5,15 +5,11 @@ FROM ${BASE_BUILDER_PYTHON_IMAGE} AS base
|
|
|
5
5
|
|
|
6
6
|
ARG BUILD_CONTEXT
|
|
7
7
|
ARG BUILDPLATFORM
|
|
8
|
-
ARG PROMETHEUS_MULTIPROC_DIR=/tmp/ocean/prometheus/metrics
|
|
9
8
|
|
|
10
9
|
ENV LIBRDKAFKA_VERSION=2.8.2 \
|
|
11
10
|
PYTHONUNBUFFERED=1 \
|
|
12
11
|
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
|
13
|
-
PIP_ROOT_USER_ACTION=ignore
|
|
14
|
-
PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR}
|
|
15
|
-
|
|
16
|
-
RUN mkdir -p ${PROMETHEUS_MULTIPROC_DIR}
|
|
12
|
+
PIP_ROOT_USER_ACTION=ignore
|
|
17
13
|
|
|
18
14
|
WORKDIR /app
|
|
19
15
|
|
|
@@ -25,8 +21,12 @@ FROM ${BASE_RUNNER_PYTHON_IMAGE} AS prod
|
|
|
25
21
|
|
|
26
22
|
ARG INTEGRATION_VERSION
|
|
27
23
|
ARG BUILD_CONTEXT
|
|
24
|
+
ARG PROMETHEUS_MULTIPROC_DIR=/tmp/ocean/prometheus/metrics
|
|
28
25
|
|
|
29
|
-
ENV LIBRDKAFKA_VERSION=2.8.2
|
|
26
|
+
ENV LIBRDKAFKA_VERSION=2.8.2 \
|
|
27
|
+
PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR}
|
|
28
|
+
|
|
29
|
+
RUN mkdir -p ${PROMETHEUS_MULTIPROC_DIR}
|
|
30
30
|
|
|
31
31
|
RUN apt-get update \
|
|
32
32
|
&& apt-get install -y \
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from typing import Any, Literal
|
|
3
3
|
from urllib.parse import quote_plus
|
|
4
|
-
|
|
4
|
+
import json
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
7
|
from loguru import logger
|
|
@@ -12,11 +12,19 @@ from port_ocean.clients.port.utils import (
|
|
|
12
12
|
handle_port_status_code,
|
|
13
13
|
PORT_HTTP_MAX_CONNECTIONS_LIMIT,
|
|
14
14
|
)
|
|
15
|
-
from port_ocean.core.models import
|
|
15
|
+
from port_ocean.core.models import (
|
|
16
|
+
BulkUpsertResponse,
|
|
17
|
+
Entity,
|
|
18
|
+
PortAPIErrorMessage,
|
|
19
|
+
)
|
|
16
20
|
from starlette import status
|
|
17
21
|
|
|
18
22
|
from port_ocean.helpers.metric.metric import MetricPhase, MetricType
|
|
19
23
|
|
|
24
|
+
ENTITIES_BULK_SAMPLES_SIZE = 10
|
|
25
|
+
ENTITIES_BULK_ESTIMATED_SIZE_MULTIPLIER = 1.5
|
|
26
|
+
ENTITIES_BULK_MINIMUM_BATCH_SIZE = 1
|
|
27
|
+
|
|
20
28
|
|
|
21
29
|
class EntityClientMixin:
|
|
22
30
|
def __init__(self, auth: PortAuthentication, client: httpx.AsyncClient):
|
|
@@ -29,6 +37,42 @@ class EntityClientMixin:
|
|
|
29
37
|
round(0.5 * PORT_HTTP_MAX_CONNECTIONS_LIMIT)
|
|
30
38
|
) # 50% of the max connections limit in order to avoid overloading port
|
|
31
39
|
|
|
40
|
+
def calculate_entities_batch_size(self, entities: list[Entity]) -> int:
|
|
41
|
+
"""
|
|
42
|
+
Calculate the optimal batch size based on entity size and configured limits.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
entities: List of entities to calculate batch size for
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
int: The optimal batch size to use
|
|
49
|
+
"""
|
|
50
|
+
if not entities:
|
|
51
|
+
return ENTITIES_BULK_MINIMUM_BATCH_SIZE
|
|
52
|
+
|
|
53
|
+
# Calculate average entity size from a sample
|
|
54
|
+
SAMPLE_SIZE = min(ENTITIES_BULK_SAMPLES_SIZE, len(entities))
|
|
55
|
+
sample_entities = entities[:SAMPLE_SIZE]
|
|
56
|
+
average_entity_size = (
|
|
57
|
+
sum(
|
|
58
|
+
len(json.dumps(entity.dict(exclude_unset=True, by_alias=True)).encode())
|
|
59
|
+
for entity in sample_entities
|
|
60
|
+
)
|
|
61
|
+
/ SAMPLE_SIZE
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Use a conservative estimate to ensure we stay under the limit
|
|
65
|
+
estimated_entity_size = int(
|
|
66
|
+
average_entity_size * ENTITIES_BULK_ESTIMATED_SIZE_MULTIPLIER
|
|
67
|
+
)
|
|
68
|
+
max_entities_per_batch = min(
|
|
69
|
+
ocean.config.upsert_entities_batch_max_length,
|
|
70
|
+
ocean.config.upsert_entities_batch_max_size_in_bytes
|
|
71
|
+
// estimated_entity_size,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return max(ENTITIES_BULK_MINIMUM_BATCH_SIZE, max_entities_per_batch)
|
|
75
|
+
|
|
32
76
|
async def upsert_entity(
|
|
33
77
|
self,
|
|
34
78
|
entity: Entity,
|
|
@@ -124,38 +168,145 @@ class EntityClientMixin:
|
|
|
124
168
|
return None
|
|
125
169
|
return self._reduce_entity(result_entity)
|
|
126
170
|
|
|
127
|
-
|
|
128
|
-
|
|
171
|
+
async def upsert_entities_bulk(
|
|
172
|
+
self,
|
|
173
|
+
blueprint: str,
|
|
174
|
+
entities: list[Entity],
|
|
175
|
+
request_options: RequestOptions,
|
|
176
|
+
user_agent_type: UserAgentType | None = None,
|
|
177
|
+
should_raise: bool = True,
|
|
178
|
+
) -> list[tuple[bool | None, Entity]] | httpx.HTTPStatusError:
|
|
129
179
|
"""
|
|
130
|
-
|
|
131
|
-
This helps save memory by removing unnecessary data.
|
|
180
|
+
This function upserts a list of entities into Port.
|
|
132
181
|
|
|
133
|
-
|
|
134
|
-
|
|
182
|
+
Usage:
|
|
183
|
+
```python
|
|
184
|
+
upsertedEntities = await self.context.port_client.upsert_entities_batch(
|
|
185
|
+
entities,
|
|
186
|
+
event.port_app_config.get_port_request_options(),
|
|
187
|
+
user_agent_type,
|
|
188
|
+
should_raise=False,
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
:param blueprint: The blueprint of the entities to be upserted
|
|
192
|
+
:param entities: A list of Entities to be upserted
|
|
193
|
+
:param request_options: A dictionary specifying how to upsert the entity
|
|
194
|
+
:param user_agent_type: a UserAgentType specifying who is preforming the action
|
|
195
|
+
:param should_raise: A boolean specifying whether the error should be raised or handled silently
|
|
196
|
+
:return: A list of tuples where each tuple contains:
|
|
197
|
+
- First value: True if entity was created successfully, False if there was an error, None if there was an error and the entity use search identifier
|
|
198
|
+
- Second value: The original entity (if failed) or the reduced entity with updated identifier (if successful)
|
|
199
|
+
:return: httpx.HTTPStatusError if there was an HTTP error and should_raise is False
|
|
200
|
+
"""
|
|
201
|
+
validation_only = request_options["validation_only"]
|
|
202
|
+
async with self.semaphore:
|
|
203
|
+
logger.debug(
|
|
204
|
+
f"{'Validating' if validation_only else 'Upserting'} {len(entities)} of blueprint: {blueprint}"
|
|
205
|
+
)
|
|
206
|
+
headers = await self.auth.headers(user_agent_type)
|
|
207
|
+
response = await self.client.post(
|
|
208
|
+
f"{self.auth.api_url}/blueprints/{blueprint}/entities/bulk",
|
|
209
|
+
json={
|
|
210
|
+
"entities": [
|
|
211
|
+
entity.dict(exclude_unset=True, by_alias=True)
|
|
212
|
+
for entity in entities
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
headers=headers,
|
|
216
|
+
params={
|
|
217
|
+
"upsert": "true",
|
|
218
|
+
"merge": str(request_options["merge"]).lower(),
|
|
219
|
+
"create_missing_related_entities": str(
|
|
220
|
+
request_options["create_missing_related_entities"]
|
|
221
|
+
).lower(),
|
|
222
|
+
"validation_only": str(validation_only).lower(),
|
|
223
|
+
},
|
|
224
|
+
extensions={"retryable": True},
|
|
225
|
+
)
|
|
226
|
+
if response.is_error:
|
|
227
|
+
logger.error(
|
|
228
|
+
f"Error {'Validating' if validation_only else 'Upserting'} "
|
|
229
|
+
f"{len(entities)} entities of "
|
|
230
|
+
f"blueprint: {blueprint}"
|
|
231
|
+
)
|
|
232
|
+
handle_port_status_code(response, should_raise)
|
|
233
|
+
return httpx.HTTPStatusError(
|
|
234
|
+
f"HTTP {response.status_code}",
|
|
235
|
+
request=response.request,
|
|
236
|
+
response=response,
|
|
237
|
+
)
|
|
238
|
+
handle_port_status_code(response, should_raise)
|
|
239
|
+
result = response.json()
|
|
135
240
|
|
|
136
|
-
|
|
137
|
-
|
|
241
|
+
return self._parse_upsert_entities_batch_response(entities, result)
|
|
242
|
+
|
|
243
|
+
def _parse_upsert_entities_batch_response(
|
|
244
|
+
self,
|
|
245
|
+
entities: list[Entity],
|
|
246
|
+
result: BulkUpsertResponse,
|
|
247
|
+
) -> list[tuple[bool | None, Entity]]:
|
|
138
248
|
"""
|
|
139
|
-
|
|
140
|
-
identifier=entity.identifier, blueprint=entity.blueprint
|
|
141
|
-
)
|
|
249
|
+
Parse the response from a bulk upsert operation and map it to the original entities.
|
|
142
250
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
251
|
+
:param entities: The original entities
|
|
252
|
+
:param result: The response from the bulk upsert operation
|
|
253
|
+
:return: A list of tuples containing the success status and the entity
|
|
254
|
+
"""
|
|
255
|
+
index_to_entity = {i: entity for i, entity in enumerate(entities)}
|
|
256
|
+
successful_entities = {
|
|
257
|
+
entity_result["index"]: entity_result
|
|
258
|
+
for entity_result in result.get("entities", [])
|
|
149
259
|
}
|
|
150
|
-
|
|
260
|
+
error_entities = {error["index"]: error for error in result.get("errors", [])}
|
|
261
|
+
|
|
262
|
+
batch_results: list[tuple[bool | None, Entity]] = []
|
|
263
|
+
for entity_index, original_entity in index_to_entity.items():
|
|
264
|
+
reduced_entity = self._reduce_entity(original_entity)
|
|
265
|
+
if entity_index in successful_entities:
|
|
266
|
+
ocean.metrics.inc_metric(
|
|
267
|
+
name=MetricType.OBJECT_COUNT_NAME,
|
|
268
|
+
labels=[
|
|
269
|
+
ocean.metrics.current_resource_kind(),
|
|
270
|
+
MetricPhase.LOAD,
|
|
271
|
+
MetricPhase.LoadResult.LOADED,
|
|
272
|
+
],
|
|
273
|
+
value=1,
|
|
274
|
+
)
|
|
275
|
+
success_entity = successful_entities[entity_index]
|
|
276
|
+
# Create a copy of the original entity with the new identifier
|
|
277
|
+
updated_entity = reduced_entity.copy()
|
|
278
|
+
updated_entity.identifier = success_entity["identifier"]
|
|
279
|
+
batch_results.append((True, updated_entity))
|
|
280
|
+
elif entity_index in error_entities:
|
|
281
|
+
ocean.metrics.inc_metric(
|
|
282
|
+
name=MetricType.OBJECT_COUNT_NAME,
|
|
283
|
+
labels=[
|
|
284
|
+
ocean.metrics.current_resource_kind(),
|
|
285
|
+
MetricPhase.LOAD,
|
|
286
|
+
MetricPhase.LoadResult.FAILED,
|
|
287
|
+
],
|
|
288
|
+
value=1,
|
|
289
|
+
)
|
|
290
|
+
error = error_entities[entity_index]
|
|
291
|
+
if (
|
|
292
|
+
error.get("identifier") == "unknown"
|
|
293
|
+
): # when using the search identifier we might not have an actual identifier
|
|
294
|
+
batch_results.append((None, reduced_entity))
|
|
295
|
+
else:
|
|
296
|
+
batch_results.append((False, reduced_entity))
|
|
297
|
+
else:
|
|
298
|
+
batch_results.append((False, reduced_entity))
|
|
299
|
+
|
|
300
|
+
return batch_results
|
|
151
301
|
|
|
152
|
-
async def
|
|
302
|
+
async def _upsert_entities_batch_individually(
|
|
153
303
|
self,
|
|
154
304
|
entities: list[Entity],
|
|
155
305
|
request_options: RequestOptions,
|
|
156
306
|
user_agent_type: UserAgentType | None = None,
|
|
157
307
|
should_raise: bool = True,
|
|
158
308
|
) -> list[tuple[bool, Entity]]:
|
|
309
|
+
entities_results: list[tuple[bool, Entity]] = []
|
|
159
310
|
modified_entities_results = await asyncio.gather(
|
|
160
311
|
*(
|
|
161
312
|
self.upsert_entity(
|
|
@@ -169,17 +320,102 @@ class EntityClientMixin:
|
|
|
169
320
|
return_exceptions=True,
|
|
170
321
|
)
|
|
171
322
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
elif result is False:
|
|
323
|
+
for original_entity, single_result in zip(entities, modified_entities_results):
|
|
324
|
+
if isinstance(single_result, Exception) and should_raise:
|
|
325
|
+
raise single_result
|
|
326
|
+
elif isinstance(single_result, Entity):
|
|
327
|
+
entities_results.append((True, single_result))
|
|
328
|
+
elif single_result is False:
|
|
179
329
|
entities_results.append((False, original_entity))
|
|
180
330
|
|
|
181
331
|
return entities_results
|
|
182
332
|
|
|
333
|
+
async def upsert_entities_in_batches(
|
|
334
|
+
self,
|
|
335
|
+
entities: list[Entity],
|
|
336
|
+
request_options: RequestOptions,
|
|
337
|
+
user_agent_type: UserAgentType | None = None,
|
|
338
|
+
should_raise: bool = True,
|
|
339
|
+
) -> list[tuple[bool, Entity]]:
|
|
340
|
+
"""
|
|
341
|
+
This function upserts a list of entities into Port in batches.
|
|
342
|
+
The batch size is calculated based on both the number of entities and their size.
|
|
343
|
+
Batches are processed in parallel using asyncio.gather, with concurrency controlled by the semaphore.
|
|
344
|
+
|
|
345
|
+
:param entities: A list of Entities to be upserted
|
|
346
|
+
:param request_options: A dictionary specifying how to upsert the entity
|
|
347
|
+
:param user_agent_type: a UserAgentType specifying who is preforming the action
|
|
348
|
+
:param should_raise: A boolean specifying whether the error should be raised or handled silently
|
|
349
|
+
:return: A list of tuples where each tuple contains:
|
|
350
|
+
- First value: True if entity was created successfully, False if there was an error
|
|
351
|
+
- Second value: The reduced entity with updated identifier (if successful) or the original entity (if failed)
|
|
352
|
+
"""
|
|
353
|
+
entities_results: list[tuple[bool, Entity]] = []
|
|
354
|
+
blueprint = entities[0].blueprint
|
|
355
|
+
|
|
356
|
+
if ocean.config.bulk_upserts_enabled:
|
|
357
|
+
bulk_size = self.calculate_entities_batch_size(entities)
|
|
358
|
+
bulks = [
|
|
359
|
+
entities[i : i + bulk_size] for i in range(0, len(entities), bulk_size)
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
bulk_results = await asyncio.gather(
|
|
363
|
+
*(
|
|
364
|
+
self.upsert_entities_bulk(
|
|
365
|
+
blueprint,
|
|
366
|
+
bulk,
|
|
367
|
+
request_options,
|
|
368
|
+
user_agent_type,
|
|
369
|
+
should_raise=should_raise,
|
|
370
|
+
)
|
|
371
|
+
for bulk in bulks
|
|
372
|
+
),
|
|
373
|
+
return_exceptions=True,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
for bulk, bulk_result in zip(bulks, bulk_results):
|
|
377
|
+
if isinstance(bulk_result, httpx.HTTPStatusError) or isinstance(
|
|
378
|
+
bulk_result, Exception
|
|
379
|
+
):
|
|
380
|
+
if should_raise:
|
|
381
|
+
raise bulk_result
|
|
382
|
+
# If should_raise is False, retry batch in sequential order as a fallback only for 413 errors
|
|
383
|
+
if (
|
|
384
|
+
isinstance(bulk_result, httpx.HTTPStatusError)
|
|
385
|
+
and bulk_result.response.status_code == 413
|
|
386
|
+
):
|
|
387
|
+
individual_upsert_results = (
|
|
388
|
+
await self._upsert_entities_batch_individually(
|
|
389
|
+
bulk, request_options, user_agent_type, should_raise
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
entities_results.extend(individual_upsert_results)
|
|
393
|
+
else:
|
|
394
|
+
# For other errors, mark all entities in the batch as failed
|
|
395
|
+
for entity in bulk:
|
|
396
|
+
failed_result: tuple[bool, Entity] = (
|
|
397
|
+
False,
|
|
398
|
+
self._reduce_entity(entity),
|
|
399
|
+
)
|
|
400
|
+
entities_results.append(failed_result)
|
|
401
|
+
elif isinstance(bulk_result, list):
|
|
402
|
+
for status, entity in bulk_result:
|
|
403
|
+
if (
|
|
404
|
+
status is not None
|
|
405
|
+
): # when using the search identifier we might not have an actual identifier
|
|
406
|
+
bulk_result_tuple: tuple[bool, Entity] = (
|
|
407
|
+
bool(status),
|
|
408
|
+
entity,
|
|
409
|
+
)
|
|
410
|
+
entities_results.append(bulk_result_tuple)
|
|
411
|
+
else:
|
|
412
|
+
individual_upsert_results = await self._upsert_entities_batch_individually(
|
|
413
|
+
entities, request_options, user_agent_type, should_raise
|
|
414
|
+
)
|
|
415
|
+
entities_results.extend(individual_upsert_results)
|
|
416
|
+
|
|
417
|
+
return entities_results
|
|
418
|
+
|
|
183
419
|
async def delete_entity(
|
|
184
420
|
self,
|
|
185
421
|
entity: Entity,
|
|
@@ -307,3 +543,28 @@ class EntityClientMixin:
|
|
|
307
543
|
"rules": [{"combinator": "or", "rules": search_rules}],
|
|
308
544
|
},
|
|
309
545
|
)
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
def _reduce_entity(entity: Entity) -> Entity:
|
|
549
|
+
"""
|
|
550
|
+
Reduces an entity to only keep identifier, blueprint and processed relations.
|
|
551
|
+
This helps save memory by removing unnecessary data.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
entity: The entity to reduce
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Entity: A new entity with only the essential data
|
|
558
|
+
"""
|
|
559
|
+
reduced_entity = Entity(
|
|
560
|
+
identifier=entity.identifier, blueprint=entity.blueprint
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Turning dict typed relations (raw search relations) is required
|
|
564
|
+
# for us to be able to successfully calculate the participation related entities
|
|
565
|
+
# and ignore the ones that don't as they weren't upserted
|
|
566
|
+
reduced_entity.relations = {
|
|
567
|
+
key: None if isinstance(relation, dict) else relation
|
|
568
|
+
for key, relation in entity.relations.items()
|
|
569
|
+
}
|
|
570
|
+
return reduced_entity
|
port_ocean/config/dynamic.py
CHANGED
|
@@ -51,7 +51,7 @@ def default_config_factory(configurations: Any) -> Type[BaseModel]:
|
|
|
51
51
|
case _:
|
|
52
52
|
raise ValueError(f"Unknown type: {config.type}")
|
|
53
53
|
|
|
54
|
-
default = ... if config.required else None
|
|
54
|
+
default: Any = ... if config.required else None
|
|
55
55
|
if config.default is not None:
|
|
56
56
|
default = parse_obj_as(field_type, config.default)
|
|
57
57
|
fields[decamelize(config.name)] = (
|
port_ocean/config/settings.py
CHANGED
|
@@ -101,6 +101,10 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
|
|
|
101
101
|
caching_storage_mode: Optional[CachingStorageMode] = Field(default=None)
|
|
102
102
|
process_execution_mode: Optional[ProcessExecutionMode] = Field(default=None)
|
|
103
103
|
|
|
104
|
+
upsert_entities_batch_max_length: int = 20
|
|
105
|
+
upsert_entities_batch_max_size_in_bytes: int = 1024 * 1024
|
|
106
|
+
bulk_upserts_enabled: bool = False
|
|
107
|
+
|
|
104
108
|
@validator("metrics", pre=True)
|
|
105
109
|
def validate_metrics(cls, v: Any) -> MetricsSettings | dict[str, Any] | None:
|
|
106
110
|
if v is None:
|
|
@@ -127,7 +127,7 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
|
|
|
127
127
|
modified_entities: list[Entity] = []
|
|
128
128
|
upserted_entities: list[tuple[bool, Entity]] = []
|
|
129
129
|
|
|
130
|
-
upserted_entities = await self.context.port_client.
|
|
130
|
+
upserted_entities = await self.context.port_client.upsert_entities_in_batches(
|
|
131
131
|
entities,
|
|
132
132
|
event.port_app_config.get_port_request_options(),
|
|
133
133
|
user_agent_type,
|
|
@@ -33,7 +33,7 @@ from port_ocean.core.ocean_types import (
|
|
|
33
33
|
CalculationResult,
|
|
34
34
|
)
|
|
35
35
|
from port_ocean.core.utils.utils import resolve_entities_diff, zip_and_sum, gather_and_split_errors_from_results
|
|
36
|
-
from port_ocean.exceptions.core import OceanAbortException
|
|
36
|
+
from port_ocean.exceptions.core import IntegrationSubProcessFailedException, OceanAbortException
|
|
37
37
|
from port_ocean.helpers.metric.metric import MetricResourceKind, SyncState, MetricType, MetricPhase
|
|
38
38
|
from port_ocean.helpers.metric.utils import TimeMetric, TimeMetricWithResourceKind
|
|
39
39
|
from port_ocean.utils.ipc import FileIPC
|
|
@@ -626,14 +626,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
626
626
|
id = uuid.uuid4()
|
|
627
627
|
logger.info(f"Starting subprocess with id {id}")
|
|
628
628
|
file_ipc_map = {
|
|
629
|
-
"process_resource": FileIPC(id, "process_resource",([],[])),
|
|
629
|
+
"process_resource": FileIPC(id, "process_resource",([],[IntegrationSubProcessFailedException(f"Subprocess failed for {resource.kind} with index {index}")])),
|
|
630
630
|
"topological_entities": FileIPC(id, "topological_entities",[]),
|
|
631
631
|
}
|
|
632
632
|
process = ProcessWrapper(target=self.process_resource_in_subprocess, args=(file_ipc_map,resource,index,user_agent_type))
|
|
633
633
|
process.start()
|
|
634
634
|
await process.join_async()
|
|
635
635
|
|
|
636
|
-
|
|
637
636
|
event.entity_topological_sorter.entities.extend(file_ipc_map["topological_entities"].load())
|
|
638
637
|
return file_ipc_map["process_resource"].load()
|
|
639
638
|
|
port_ocean/core/models.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from enum import Enum, StrEnum
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from pydantic.fields import Field
|
|
@@ -67,6 +67,25 @@ class Entity(BaseModel):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class EntityBulkResult(TypedDict):
|
|
71
|
+
identifier: str
|
|
72
|
+
index: int
|
|
73
|
+
created: bool
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class EntityBulkError(TypedDict):
|
|
77
|
+
identifier: str
|
|
78
|
+
index: int
|
|
79
|
+
statusCode: int
|
|
80
|
+
error: str
|
|
81
|
+
message: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class BulkUpsertResponse(TypedDict):
|
|
85
|
+
entities: list[EntityBulkResult]
|
|
86
|
+
errors: list[EntityBulkError]
|
|
87
|
+
|
|
88
|
+
|
|
70
89
|
class BlueprintRelation(BaseModel):
|
|
71
90
|
many: bool
|
|
72
91
|
required: bool
|
port_ocean/exceptions/core.py
CHANGED
|
@@ -123,6 +123,7 @@ class Metrics:
|
|
|
123
123
|
self.registry = prometheus_client.CollectorRegistry()
|
|
124
124
|
if multiprocessing_enabled:
|
|
125
125
|
multiprocess.MultiProcessCollector(self.registry)
|
|
126
|
+
self.multiprocessing_enabled = multiprocessing_enabled
|
|
126
127
|
self.metrics: dict[str, Gauge] = {}
|
|
127
128
|
self.load_metrics()
|
|
128
129
|
self._integration_version: Optional[str] = None
|
|
@@ -213,7 +214,8 @@ class Metrics:
|
|
|
213
214
|
logger.error(f"Failed to cleanup prometheus metrics: {e}")
|
|
214
215
|
|
|
215
216
|
def initialize_metrics(self, kind_blockes: list[str]) -> None:
|
|
216
|
-
self.
|
|
217
|
+
if self.multiprocessing_enabled:
|
|
218
|
+
self.cleanup_prometheus_metrics()
|
|
217
219
|
for kind in kind_blockes:
|
|
218
220
|
self.set_metric(MetricType.SUCCESS_NAME, [kind, MetricPhase.RESYNC], 0)
|
|
219
221
|
self.set_metric(MetricType.DURATION_NAME, [kind, MetricPhase.RESYNC], 0)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
from unittest.mock import MagicMock, patch
|
|
1
|
+
from typing import Any, List, Generator
|
|
2
|
+
from unittest.mock import MagicMock, patch, AsyncMock
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
@@ -7,50 +7,168 @@ from port_ocean.clients.port.mixins.entities import EntityClientMixin
|
|
|
7
7
|
from port_ocean.core.models import Entity
|
|
8
8
|
from httpx import ReadTimeout
|
|
9
9
|
|
|
10
|
+
# Mock the ocean context at module level
|
|
11
|
+
pytestmark = pytest.mark.usefixtures("mock_ocean")
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
errored_entity_identifier: str = "a"
|
|
12
|
-
expected_result_entities = [
|
|
15
|
+
expected_result_entities: List[Entity] = [
|
|
13
16
|
Entity(identifier="b", blueprint="b"),
|
|
14
17
|
Entity(identifier="c", blueprint="c"),
|
|
15
18
|
]
|
|
16
|
-
|
|
19
|
+
expected_result_entities_with_exception: List[Entity] = []
|
|
20
|
+
all_entities: List[Entity] = [
|
|
17
21
|
Entity(identifier=errored_entity_identifier, blueprint="a")
|
|
18
22
|
] + expected_result_entities
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
@pytest.fixture(autouse=True)
|
|
26
|
+
def mock_ocean() -> Generator[MagicMock, None, None]:
|
|
27
|
+
with patch("port_ocean.clients.port.mixins.entities.ocean") as mock_ocean:
|
|
28
|
+
mock_ocean.config.upsert_entities_batch_max_length = 20
|
|
29
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1024 * 1024 # 1MB
|
|
30
|
+
yield mock_ocean
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def mock_upsert_entities_bulk(
|
|
34
|
+
blueprint: str, entities: list[Entity], *args: Any, **kwargs: Any
|
|
35
|
+
) -> list[tuple[bool | None, Entity]]:
|
|
36
|
+
results: list[tuple[bool | None, Entity]] = []
|
|
37
|
+
for entity in entities:
|
|
38
|
+
if entity.identifier == errored_entity_identifier:
|
|
39
|
+
results.append((False, entity))
|
|
40
|
+
else:
|
|
41
|
+
results.append((True, entity))
|
|
42
|
+
return results
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def mock_exception_upsert_entities_bulk(
|
|
46
|
+
blueprint: str, entities: list[Entity], *args: Any, **kwargs: Any
|
|
47
|
+
) -> list[tuple[bool | None, Entity]]:
|
|
48
|
+
for entity in entities:
|
|
49
|
+
if entity.identifier == errored_entity_identifier:
|
|
50
|
+
raise ReadTimeout("")
|
|
51
|
+
return [(True, entity) for entity in entities]
|
|
26
52
|
|
|
27
53
|
|
|
28
54
|
@pytest.fixture
|
|
29
55
|
async def entity_client(monkeypatch: Any) -> EntityClientMixin:
|
|
30
|
-
# Arrange
|
|
31
56
|
entity_client = EntityClientMixin(auth=MagicMock(), client=MagicMock())
|
|
32
|
-
|
|
57
|
+
mock = AsyncMock()
|
|
58
|
+
mock.side_effect = mock_upsert_entities_bulk
|
|
59
|
+
monkeypatch.setattr(entity_client, "upsert_entities_bulk", mock)
|
|
33
60
|
|
|
34
61
|
return entity_client
|
|
35
62
|
|
|
36
63
|
|
|
64
|
+
def test_calculate_entities_batch_size_empty_list(
|
|
65
|
+
entity_client: EntityClientMixin,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Test that empty list returns batch size of 1"""
|
|
68
|
+
assert entity_client.calculate_entities_batch_size([]) == 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_calculate_entities_batch_size_small_entities(
|
|
72
|
+
entity_client: EntityClientMixin,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Test that small entities return max batch size"""
|
|
75
|
+
small_entities = [
|
|
76
|
+
Entity(identifier=f"small_{i}", blueprint="test", properties={"small": "value"})
|
|
77
|
+
for i in range(30)
|
|
78
|
+
]
|
|
79
|
+
# Small entities should allow max batch size
|
|
80
|
+
assert entity_client.calculate_entities_batch_size(small_entities) == 20
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_calculate_entities_batch_size_large_entities(
|
|
84
|
+
entity_client: EntityClientMixin,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Test that large entities reduce batch size"""
|
|
87
|
+
large_entities = [
|
|
88
|
+
Entity(
|
|
89
|
+
identifier=f"large_{i}",
|
|
90
|
+
blueprint="test",
|
|
91
|
+
properties={"large": "x" * (100 * 1024)}, # 100KB per entity
|
|
92
|
+
)
|
|
93
|
+
for i in range(30)
|
|
94
|
+
]
|
|
95
|
+
# With 1MB limit and 100KB per entity (plus overhead), should get ~9 entities per batch
|
|
96
|
+
batch_size = entity_client.calculate_entities_batch_size(large_entities)
|
|
97
|
+
assert 5 <= batch_size <= 15
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_calculate_entities_batch_size_mixed_entities(
|
|
101
|
+
entity_client: EntityClientMixin,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Test that mixed size entities calculate correct batch size"""
|
|
104
|
+
mixed_entities = [
|
|
105
|
+
Entity(identifier=f"small_{i}", blueprint="test", properties={"small": "value"})
|
|
106
|
+
for i in range(10)
|
|
107
|
+
] + [
|
|
108
|
+
Entity(
|
|
109
|
+
identifier=f"large_{i}",
|
|
110
|
+
blueprint="test",
|
|
111
|
+
properties={"large": "x" * (50 * 1024)}, # 50KB per entity
|
|
112
|
+
)
|
|
113
|
+
for i in range(10)
|
|
114
|
+
]
|
|
115
|
+
# With 1MB limit and mixed sizes, should get a reasonable batch size
|
|
116
|
+
batch_size = entity_client.calculate_entities_batch_size(mixed_entities)
|
|
117
|
+
assert 15 <= batch_size <= 20
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_calculate_entities_batch_size_single_large_entity(
|
|
121
|
+
entity_client: EntityClientMixin,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Test that single very large entity returns batch size of 1"""
|
|
124
|
+
large_entity = Entity(
|
|
125
|
+
identifier="huge",
|
|
126
|
+
blueprint="test",
|
|
127
|
+
properties={"huge": "x" * (2 * 1024 * 1024)}, # 2MB entity
|
|
128
|
+
)
|
|
129
|
+
# Even though entity is larger than limit, we return 1 to ensure at least one entity is processed
|
|
130
|
+
assert entity_client.calculate_entities_batch_size([large_entity]) == 1
|
|
131
|
+
|
|
132
|
+
|
|
37
133
|
async def test_batch_upsert_entities_read_timeout_should_raise_false(
|
|
38
134
|
entity_client: EntityClientMixin,
|
|
39
135
|
) -> None:
|
|
40
136
|
with patch("port_ocean.context.event.event", MagicMock()):
|
|
41
|
-
result_entities = await entity_client.
|
|
137
|
+
result_entities = await entity_client.upsert_entities_in_batches(
|
|
42
138
|
entities=all_entities, request_options=MagicMock(), should_raise=False
|
|
43
139
|
)
|
|
44
|
-
|
|
140
|
+
# Only get entities that were successfully upserted (status is True)
|
|
141
|
+
entities_only = [entity for status, entity in result_entities if status is True]
|
|
45
142
|
|
|
46
143
|
assert entities_only == expected_result_entities
|
|
47
144
|
|
|
48
145
|
|
|
146
|
+
async def test_batch_upsert_entities_read_timeout_should_raise_false_with_exception(
|
|
147
|
+
entity_client: EntityClientMixin,
|
|
148
|
+
monkeypatch: Any,
|
|
149
|
+
) -> None:
|
|
150
|
+
with patch("port_ocean.context.event.event", MagicMock()):
|
|
151
|
+
# Override the mock for this test to use the exception-throwing version
|
|
152
|
+
mock = AsyncMock(side_effect=mock_exception_upsert_entities_bulk)
|
|
153
|
+
monkeypatch.setattr(entity_client, "upsert_entities_bulk", mock)
|
|
154
|
+
result_entities = await entity_client.upsert_entities_in_batches(
|
|
155
|
+
entities=all_entities, request_options=MagicMock(), should_raise=False
|
|
156
|
+
)
|
|
157
|
+
# Only get entities that were successfully upserted (status is True)
|
|
158
|
+
entities_only = [entity for status, entity in result_entities if status is True]
|
|
159
|
+
|
|
160
|
+
assert entities_only == expected_result_entities_with_exception
|
|
161
|
+
|
|
162
|
+
|
|
49
163
|
async def test_batch_upsert_entities_read_timeout_should_raise_true(
|
|
50
164
|
entity_client: EntityClientMixin,
|
|
165
|
+
monkeypatch: Any,
|
|
51
166
|
) -> None:
|
|
52
167
|
with patch("port_ocean.context.event.event", MagicMock()):
|
|
168
|
+
# Override the mock for this test to use the exception-throwing version
|
|
169
|
+
mock = AsyncMock(side_effect=mock_exception_upsert_entities_bulk)
|
|
170
|
+
monkeypatch.setattr(entity_client, "upsert_entities_bulk", mock)
|
|
53
171
|
with pytest.raises(ReadTimeout):
|
|
54
|
-
await entity_client.
|
|
172
|
+
await entity_client.upsert_entities_in_batches(
|
|
55
173
|
entities=all_entities, request_options=MagicMock(), should_raise=True
|
|
56
174
|
)
|
|
@@ -35,24 +35,59 @@ def mock_http_client() -> MagicMock:
|
|
|
35
35
|
mock_upserted_entities = []
|
|
36
36
|
|
|
37
37
|
async def post(url: str, *args: Any, **kwargs: Any) -> Response:
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if "/bulk" in url:
|
|
39
|
+
success_entities = []
|
|
40
|
+
failed_entities = []
|
|
41
|
+
entities_body = kwargs.get("json", {})
|
|
42
|
+
entities = entities_body.get("entities", [])
|
|
43
|
+
for index, entity in enumerate(entities):
|
|
44
|
+
if entity.get("properties", {}).get("mock_is_to_fail", False):
|
|
45
|
+
failed_entities.append(
|
|
46
|
+
{
|
|
47
|
+
"identifier": entity.get("identifier"),
|
|
48
|
+
"index": index,
|
|
49
|
+
"statusCode": 404,
|
|
50
|
+
"error": "not_found",
|
|
51
|
+
"message": "Entity not found",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
mock_upserted_entities.append(
|
|
56
|
+
f"{entity.get('identifier')}-{entity.get('blueprint')}"
|
|
57
|
+
)
|
|
58
|
+
success_entities.append(
|
|
59
|
+
(
|
|
60
|
+
{
|
|
61
|
+
"identifier": entity.get("identifier"),
|
|
62
|
+
"index": index,
|
|
63
|
+
"created": True,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
40
68
|
return Response(
|
|
41
|
-
|
|
69
|
+
207,
|
|
70
|
+
json={"entities": success_entities, "errors": failed_entities},
|
|
42
71
|
)
|
|
72
|
+
else:
|
|
73
|
+
entity = kwargs.get("json", {})
|
|
74
|
+
if entity.get("properties", {}).get("mock_is_to_fail", False):
|
|
75
|
+
return Response(
|
|
76
|
+
404, headers=MagicMock(), json={"ok": False, "error": "not_found"}
|
|
77
|
+
)
|
|
43
78
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
mock_upserted_entities.append(
|
|
80
|
+
f"{entity.get('identifier')}-{entity.get('blueprint')}"
|
|
81
|
+
)
|
|
82
|
+
return Response(
|
|
83
|
+
200,
|
|
84
|
+
json={
|
|
85
|
+
"entity": {
|
|
86
|
+
"identifier": entity.get("identifier"),
|
|
87
|
+
"blueprint": entity.get("blueprint"),
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
)
|
|
56
91
|
|
|
57
92
|
mock_http_client.post = AsyncMock(side_effect=post)
|
|
58
93
|
return mock_http_client
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from unittest.mock import Mock, patch
|
|
1
|
+
from unittest.mock import Mock, patch, AsyncMock
|
|
2
2
|
import pytest
|
|
3
3
|
from port_ocean.core.handlers.entities_state_applier.port.applier import (
|
|
4
4
|
HttpEntitiesStateApplier,
|
|
@@ -125,33 +125,29 @@ async def test_applier_with_mock_context(
|
|
|
125
125
|
mock_context: PortOceanContext,
|
|
126
126
|
mock_port_app_config: PortAppConfig,
|
|
127
127
|
) -> None:
|
|
128
|
-
# Create an applier using the mock_context fixture
|
|
129
128
|
applier = HttpEntitiesStateApplier(mock_context)
|
|
130
|
-
|
|
131
|
-
# Create test entities
|
|
132
129
|
entity = Entity(identifier="test_entity", blueprint="test_blueprint")
|
|
133
130
|
|
|
134
131
|
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
135
132
|
event.port_app_config = mock_port_app_config
|
|
133
|
+
event.entity_topological_sorter = Mock()
|
|
134
|
+
|
|
135
|
+
mock_blueprint = Mock()
|
|
136
|
+
mock_blueprint.identifier = "test_blueprint"
|
|
137
|
+
mock_blueprint.relations = {}
|
|
138
|
+
mock_get_blueprint = AsyncMock(return_value=mock_blueprint)
|
|
139
|
+
setattr(mock_ocean.port_client, "get_blueprint", mock_get_blueprint)
|
|
136
140
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
mock_post.return_value = Mock(
|
|
140
|
-
status_code=200,
|
|
141
|
-
json=lambda: {
|
|
142
|
-
"entity": {
|
|
143
|
-
"identifier": "test_entity",
|
|
144
|
-
"blueprint": "test_blueprint",
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
)
|
|
141
|
+
mock_ocean.config.upsert_entities_batch_max_length = 100
|
|
142
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1000
|
|
148
143
|
|
|
149
|
-
|
|
144
|
+
mock_upsert = AsyncMock(return_value=[(True, entity)])
|
|
145
|
+
setattr(mock_ocean.port_client, "upsert_entities_bulk", mock_upsert)
|
|
150
146
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
result = await applier.upsert([entity], UserAgentType.exporter)
|
|
148
|
+
mock_upsert.assert_called_once()
|
|
149
|
+
assert len(result) == 1
|
|
150
|
+
assert result[0].identifier == "test_entity"
|
|
155
151
|
|
|
156
152
|
|
|
157
153
|
@pytest.mark.asyncio
|
|
@@ -160,32 +156,24 @@ async def test_applier_one_not_upserted(
|
|
|
160
156
|
mock_context: PortOceanContext,
|
|
161
157
|
mock_port_app_config: PortAppConfig,
|
|
162
158
|
) -> None:
|
|
163
|
-
# Create an applier using the mock_context fixture
|
|
164
159
|
applier = HttpEntitiesStateApplier(mock_context)
|
|
165
|
-
|
|
166
|
-
# Create test entities
|
|
167
160
|
entity = Entity(identifier="test_entity", blueprint="test_blueprint")
|
|
168
161
|
|
|
169
162
|
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
170
|
-
# Mock the register_entity method
|
|
171
163
|
event.entity_topological_sorter.register_entity = Mock() # type: ignore
|
|
172
164
|
event.port_app_config = mock_port_app_config
|
|
173
165
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
166
|
+
mock_ocean.config.upsert_entities_batch_max_length = 100
|
|
167
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1000
|
|
168
|
+
|
|
169
|
+
mock_upsert = AsyncMock(return_value=[(False, entity)])
|
|
170
|
+
setattr(mock_ocean.port_client, "upsert_entities_bulk", mock_upsert)
|
|
180
171
|
|
|
181
|
-
|
|
172
|
+
result = await applier.upsert([entity], UserAgentType.exporter)
|
|
182
173
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
event.entity_topological_sorter.register_entity.assert_called_once_with(
|
|
187
|
-
entity
|
|
188
|
-
)
|
|
174
|
+
mock_upsert.assert_called_once()
|
|
175
|
+
assert len(result) == 0
|
|
176
|
+
event.entity_topological_sorter.register_entity.assert_called_once_with(entity)
|
|
189
177
|
|
|
190
178
|
|
|
191
179
|
@pytest.mark.asyncio
|
|
@@ -194,32 +182,23 @@ async def test_applier_error_upserting(
|
|
|
194
182
|
mock_context: PortOceanContext,
|
|
195
183
|
mock_port_app_config: PortAppConfig,
|
|
196
184
|
) -> None:
|
|
197
|
-
# Create an applier using the mock_context fixture
|
|
198
185
|
applier = HttpEntitiesStateApplier(mock_context)
|
|
199
|
-
|
|
200
|
-
# Create test entities
|
|
201
186
|
entity = Entity(identifier="test_entity", blueprint="test_blueprint")
|
|
202
187
|
|
|
203
188
|
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
204
|
-
# Mock the register_entity method
|
|
205
189
|
event.entity_topological_sorter.register_entity = Mock() # type: ignore
|
|
206
190
|
event.port_app_config = mock_port_app_config
|
|
207
191
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
mock_post.return_value = Mock(
|
|
211
|
-
status_code=404,
|
|
212
|
-
json=lambda: {"ok": False, "error": "not_found"},
|
|
213
|
-
)
|
|
192
|
+
mock_ocean.config.upsert_entities_batch_max_length = 100
|
|
193
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1000
|
|
214
194
|
|
|
215
|
-
|
|
195
|
+
mock_upsert = AsyncMock(return_value=[(False, entity)])
|
|
196
|
+
setattr(mock_ocean.port_client, "upsert_entities_bulk", mock_upsert)
|
|
216
197
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
entity
|
|
222
|
-
)
|
|
198
|
+
result = await applier.upsert([entity], UserAgentType.exporter)
|
|
199
|
+
mock_upsert.assert_called_once()
|
|
200
|
+
assert len(result) == 0
|
|
201
|
+
event.entity_topological_sorter.register_entity.assert_called_once_with(entity)
|
|
223
202
|
|
|
224
203
|
|
|
225
204
|
@pytest.mark.asyncio
|
|
@@ -228,31 +207,24 @@ async def test_using_create_entity_helper(
|
|
|
228
207
|
mock_context: PortOceanContext,
|
|
229
208
|
mock_port_app_config: PortAppConfig,
|
|
230
209
|
) -> None:
|
|
231
|
-
# Create the applier with the mock context
|
|
232
210
|
applier = HttpEntitiesStateApplier(mock_context)
|
|
233
|
-
|
|
234
|
-
# Create test entities using the helper function
|
|
235
211
|
entity1 = create_entity("entity1", "service", {"related_to": "entity2"}, False)
|
|
236
212
|
|
|
237
|
-
# Test that entities were created correctly
|
|
238
213
|
assert entity1.identifier == "entity1"
|
|
239
214
|
assert entity1.blueprint == "service"
|
|
240
215
|
assert entity1.relations == {"related_to": "entity2"}
|
|
241
216
|
assert entity1.properties == {"mock_is_to_fail": False}
|
|
242
217
|
|
|
243
|
-
# Test the applier with these entities
|
|
244
218
|
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
245
219
|
event.port_app_config = mock_port_app_config
|
|
246
220
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
},
|
|
253
|
-
)
|
|
221
|
+
mock_ocean.config.upsert_entities_batch_max_length = 100
|
|
222
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1000
|
|
223
|
+
|
|
224
|
+
mock_upsert = AsyncMock(return_value=[(True, entity1)])
|
|
225
|
+
setattr(mock_ocean.port_client, "upsert_entities_bulk", mock_upsert)
|
|
254
226
|
|
|
255
|
-
|
|
227
|
+
result = await applier.upsert([entity1], UserAgentType.exporter)
|
|
256
228
|
|
|
257
|
-
|
|
258
|
-
|
|
229
|
+
mock_upsert.assert_called_once()
|
|
230
|
+
assert len(result) == 1
|
|
@@ -96,6 +96,10 @@ async def test_sync_raw_mixin_self_dependency(
|
|
|
96
96
|
mock_sync_raw_mixin: SyncRawMixin,
|
|
97
97
|
mock_ocean: Ocean,
|
|
98
98
|
) -> None:
|
|
99
|
+
mock_ocean.config.upsert_entities_batch_max_length = 20
|
|
100
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1024 * 1024
|
|
101
|
+
mock_ocean.config.bulk_upserts_enabled = True
|
|
102
|
+
|
|
99
103
|
entities_params = [
|
|
100
104
|
("entity_1", "service", {"service": "entity_1"}, True),
|
|
101
105
|
("entity_2", "service", {"service": "entity_2"}, False),
|
|
@@ -148,9 +152,13 @@ async def test_sync_raw_mixin_self_dependency(
|
|
|
148
152
|
|
|
149
153
|
assert mock_order_by_entities_dependencies.call_count == 1
|
|
150
154
|
assert [
|
|
151
|
-
call[0][0][0]
|
|
155
|
+
call[0][0][0].identifier
|
|
152
156
|
for call in mock_order_by_entities_dependencies.call_args_list
|
|
153
|
-
] == [
|
|
157
|
+
] == [
|
|
158
|
+
entity.identifier
|
|
159
|
+
for entity in entities
|
|
160
|
+
if entity.identifier == "entity_1"
|
|
161
|
+
]
|
|
154
162
|
|
|
155
163
|
# Add assertions for actual metrics
|
|
156
164
|
metrics = mock_ocean.metrics.generate_metrics()
|
|
@@ -209,6 +217,10 @@ async def test_sync_raw_mixin_self_dependency(
|
|
|
209
217
|
async def test_sync_raw_mixin_circular_dependency(
|
|
210
218
|
mock_sync_raw_mixin: SyncRawMixin, mock_ocean: Ocean
|
|
211
219
|
) -> None:
|
|
220
|
+
mock_ocean.config.upsert_entities_batch_max_length = 20
|
|
221
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1024 * 1024
|
|
222
|
+
mock_ocean.config.bulk_upserts_enabled = True
|
|
223
|
+
|
|
212
224
|
entities_params = [
|
|
213
225
|
("entity_1", "service", {"service": "entity_2"}, True),
|
|
214
226
|
("entity_2", "service", {"service": "entity_1"}, True),
|
|
@@ -284,8 +296,8 @@ async def test_sync_raw_mixin_circular_dependency(
|
|
|
284
296
|
assert isinstance(raiesed_error_handle_failed[0].__cause__, CycleError)
|
|
285
297
|
assert (
|
|
286
298
|
len(mock_ocean.port_client.client.post.call_args_list) # type: ignore
|
|
287
|
-
|
|
288
|
-
==
|
|
299
|
+
- len(entities)
|
|
300
|
+
== 1
|
|
289
301
|
)
|
|
290
302
|
|
|
291
303
|
# Add assertions for actual metrics
|
|
@@ -345,7 +357,10 @@ async def test_sync_raw_mixin_circular_dependency(
|
|
|
345
357
|
async def test_sync_raw_mixin_dependency(
|
|
346
358
|
mock_sync_raw_mixin: SyncRawMixin, mock_ocean: Ocean
|
|
347
359
|
) -> None:
|
|
348
|
-
|
|
360
|
+
mock_ocean.config.upsert_entities_batch_max_length = 20
|
|
361
|
+
mock_ocean.config.upsert_entities_batch_max_size_in_bytes = 1024 * 1024
|
|
362
|
+
mock_ocean.config.bulk_upserts_enabled = True
|
|
363
|
+
|
|
349
364
|
entities_params = [
|
|
350
365
|
("entity_1", "service", {"service": "entity_3"}, True),
|
|
351
366
|
("entity_2", "service", {"service": "entity_4"}, True),
|
|
@@ -418,17 +433,20 @@ async def test_sync_raw_mixin_dependency(
|
|
|
418
433
|
), "Expected one failed entity callback due to retry logic"
|
|
419
434
|
assert event.entity_topological_sorter.get_entities.call_count == 1
|
|
420
435
|
assert len(raiesed_error_handle_failed) == 0
|
|
421
|
-
assert mock_ocean.port_client.client.post.call_count ==
|
|
436
|
+
assert mock_ocean.port_client.client.post.call_count == 6 # type: ignore
|
|
422
437
|
assert mock_order_by_entities_dependencies.call_count == 1
|
|
423
438
|
|
|
424
|
-
|
|
425
|
-
|
|
439
|
+
result_bulk = mock_ocean.port_client.client.post.call_args_list[0] # type: ignore
|
|
440
|
+
result_non_bulk = mock_ocean.port_client.client.post.call_args_list[1:6] # type: ignore
|
|
426
441
|
|
|
427
442
|
assert "-".join(
|
|
428
|
-
[
|
|
443
|
+
[
|
|
444
|
+
entity.get("identifier")
|
|
445
|
+
for entity in result_bulk[1].get("json").get("entities")
|
|
446
|
+
]
|
|
429
447
|
) == "-".join([entity.identifier for entity in entities])
|
|
430
448
|
assert "-".join(
|
|
431
|
-
[call[1].get("json").get("identifier") for call in
|
|
449
|
+
[call[1].get("json").get("identifier") for call in result_non_bulk]
|
|
432
450
|
) in (
|
|
433
451
|
"entity_3-entity_4-entity_1-entity_2-entity_5",
|
|
434
452
|
"entity_3-entity_4-entity_1-entity_5-entity_2",
|
|
@@ -892,6 +910,8 @@ async def test_on_resync_start_hooks_are_called(
|
|
|
892
910
|
resync_start_called = True
|
|
893
911
|
|
|
894
912
|
mock_sync_raw_mixin.on_resync_start(on_resync_start)
|
|
913
|
+
mock_sync_raw_mixin._get_resource_raw_results = AsyncMock(return_value=([], [])) # type: ignore
|
|
914
|
+
|
|
895
915
|
mock_ocean.metrics.report_sync_metrics = AsyncMock(return_value=None) # type: ignore
|
|
896
916
|
mock_ocean.metrics.report_kind_sync_metrics = AsyncMock(return_value=None) # type: ignore
|
|
897
917
|
mock_ocean.metrics.send_metrics_to_webhook = AsyncMock(return_value=None) # type: ignore
|
|
@@ -922,6 +942,7 @@ async def test_on_resync_complete_hooks_are_called_on_success(
|
|
|
922
942
|
|
|
923
943
|
mock_sync_raw_mixin.on_resync_complete(on_resync_complete)
|
|
924
944
|
mock_ocean.port_client.search_entities.return_value = [] # type: ignore
|
|
945
|
+
mock_sync_raw_mixin._get_resource_raw_results = AsyncMock(return_value=([], [])) # type: ignore
|
|
925
946
|
mock_ocean.metrics.report_sync_metrics = AsyncMock(return_value=None) # type: ignore
|
|
926
947
|
mock_ocean.metrics.report_kind_sync_metrics = AsyncMock(return_value=None) # type: ignore
|
|
927
948
|
mock_ocean.metrics.send_metrics_to_webhook = AsyncMock(return_value=None) # type: ignore
|
|
@@ -994,6 +1015,7 @@ async def test_multiple_on_resync_start_on_resync_complete_hooks_called_in_order
|
|
|
994
1015
|
mock_sync_raw_mixin.on_resync_complete(on_resync_complete1)
|
|
995
1016
|
mock_sync_raw_mixin.on_resync_complete(on_resync_complete2)
|
|
996
1017
|
mock_ocean.port_client.search_entities.return_value = [] # type: ignore
|
|
1018
|
+
mock_sync_raw_mixin._get_resource_raw_results = AsyncMock(return_value=([], [])) # type: ignore
|
|
997
1019
|
|
|
998
1020
|
mock_ocean.metrics.report_sync_metrics = AsyncMock(return_value=None) # type: ignore
|
|
999
1021
|
mock_ocean.metrics.report_kind_sync_metrics = AsyncMock(return_value=None) # type: ignore
|
|
@@ -21,7 +21,8 @@ def test_serialize_record_log_shape() -> None:
|
|
|
21
21
|
)
|
|
22
22
|
serialized_record = _serialize_record(record)
|
|
23
23
|
assert all(key in serialized_record for key in expected_keys)
|
|
24
|
-
|
|
24
|
+
message = serialized_record.get("message", None)
|
|
25
|
+
assert message is not None and log_message in message
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def test_serialize_record_exc_info_single_exception() -> None:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
integrations/_infra/Dockerfile.Deb,sha256=
|
|
1
|
+
integrations/_infra/Dockerfile.Deb,sha256=5mSAOr1TaqkfFPTKkB9q9BYIHi4kaR8imfBwJVZuP1M,1679
|
|
2
2
|
integrations/_infra/Dockerfile.alpine,sha256=7E4Sb-8supsCcseerHwTkuzjHZoYcaHIyxiBZ-wewo0,3482
|
|
3
3
|
integrations/_infra/Dockerfile.base.builder,sha256=ESe1PKC6itp_AuXawbLI75k1Kruny6NTANaTinxOgVs,743
|
|
4
4
|
integrations/_infra/Dockerfile.base.runner,sha256=uAcs2IsxrAAUHGXt_qULA5INr-HFguf5a5fCKiqEzbY,384
|
|
@@ -59,7 +59,7 @@ port_ocean/clients/port/authentication.py,sha256=r7r8Ag9WuwXy-CmgeOoj-PHbmJAQxhb
|
|
|
59
59
|
port_ocean/clients/port/client.py,sha256=dv0mxIOde6J-wFi1FXXZkoNPVHrZzY7RSMhNkDD9xgA,3566
|
|
60
60
|
port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
61
|
port_ocean/clients/port/mixins/blueprints.py,sha256=aMCG4zePsMSMjMLiGrU37h5z5_ElfMzTcTvqvOI5wXY,4683
|
|
62
|
-
port_ocean/clients/port/mixins/entities.py,sha256
|
|
62
|
+
port_ocean/clients/port/mixins/entities.py,sha256=OMcgJMKruxLBYrHVvzhQmkW6_4QRYi48myjiBd-BTaA,23317
|
|
63
63
|
port_ocean/clients/port/mixins/integrations.py,sha256=s6paomK9bYWW-Tu3y2OIaEGSxsXCHyhapVi4JIhhO64,11162
|
|
64
64
|
port_ocean/clients/port/mixins/migrations.py,sha256=vdL_A_NNUogvzujyaRLIoZEu5vmKDY2BxTjoGP94YzI,1467
|
|
65
65
|
port_ocean/clients/port/mixins/organization.py,sha256=A2cP5V49KnjoAXxjmnm_XGth4ftPSU0qURNfnyUyS_Y,1041
|
|
@@ -68,8 +68,8 @@ port_ocean/clients/port/types.py,sha256=nvlgiAq4WH5_F7wQbz_GAWl-faob84LVgIjZ2Ww5
|
|
|
68
68
|
port_ocean/clients/port/utils.py,sha256=osFyAjw7Y5Qf2uVSqC7_RTCQfijiL1zS74JJM0goxh0,2762
|
|
69
69
|
port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
70
|
port_ocean/config/base.py,sha256=x1gFbzujrxn7EJudRT81C6eN9WsYAb3vOHwcpcpX8Tc,6370
|
|
71
|
-
port_ocean/config/dynamic.py,sha256=
|
|
72
|
-
port_ocean/config/settings.py,sha256=
|
|
71
|
+
port_ocean/config/dynamic.py,sha256=T0AWE41tjp9fL1sgrTRwNAGlPw6xiakFp-KXWvHtu_4,2035
|
|
72
|
+
port_ocean/config/settings.py,sha256=ls8yae6j7UP_-eIx_eLAwPxSraSsaDJn9osNqz1QUUo,6588
|
|
73
73
|
port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
74
74
|
port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
|
|
75
75
|
port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -95,7 +95,7 @@ port_ocean/core/handlers/base.py,sha256=cTarblazu8yh8xz2FpB-dzDKuXxtoi143XJgPbV_
|
|
|
95
95
|
port_ocean/core/handlers/entities_state_applier/__init__.py,sha256=kgLZDCeCEzi4r-0nzW9k78haOZNf6PX7mJOUr34A4c8,173
|
|
96
96
|
port_ocean/core/handlers/entities_state_applier/base.py,sha256=5wHL0icfFAYRPqk8iV_wN49GdJ3aRUtO8tumSxBi4Wo,2268
|
|
97
97
|
port_ocean/core/handlers/entities_state_applier/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
|
-
port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=
|
|
98
|
+
port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=QzN8OWgp-i4bRwslnLo5qjS7rt93Stceq9C4FD11Vy4,6372
|
|
99
99
|
port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py,sha256=1zncwCbE-Gej0xaWKlzZgoXxOBe9bgs_YxlZ8QW3NdI,1751
|
|
100
100
|
port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=lyv6xKzhYfd6TioUgR3AVRSJqj7JpAaj1LxxU2xAqeo,1720
|
|
101
101
|
port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybem9MthOs7p1Wawac87uSXz9U8,156
|
|
@@ -121,9 +121,9 @@ port_ocean/core/integrations/mixins/events.py,sha256=2L7P3Jhp8XBqddh2_o9Cn4N261n
|
|
|
121
121
|
port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
|
|
122
122
|
port_ocean/core/integrations/mixins/live_events.py,sha256=8HklZmlyffYY_LeDe8xbt3Tb08rlLkqVhFF-2NQeJP4,4126
|
|
123
123
|
port_ocean/core/integrations/mixins/sync.py,sha256=Vm_898pLKBwfVewtwouDWsXoxcOLicnAy6pzyqqk6U8,4053
|
|
124
|
-
port_ocean/core/integrations/mixins/sync_raw.py,sha256=
|
|
124
|
+
port_ocean/core/integrations/mixins/sync_raw.py,sha256=4zNsf0QdvsLANhoUTUnf0bNbHIyV7-MqQOYIr0ES85o,34121
|
|
125
125
|
port_ocean/core/integrations/mixins/utils.py,sha256=g1XbC12dswefQ-NpcLSCqFtd_WRp2bTL98jyZ5rRbGk,3444
|
|
126
|
-
port_ocean/core/models.py,sha256=
|
|
126
|
+
port_ocean/core/models.py,sha256=NYsOBtAqRgmRTb2XYGDW31IxTHSXGRQLOF64apwUZ2Q,2872
|
|
127
127
|
port_ocean/core/ocean_types.py,sha256=4VipWFOHEh_d9LmWewQccwx1p2dtrRYW0YURVgNsAjo,1398
|
|
128
128
|
port_ocean/core/utils/entity_topological_sorter.py,sha256=MDUjM6OuDy4Xj68o-7InNN0w1jqjxeDfeY8U02vySNI,3081
|
|
129
129
|
port_ocean/core/utils/utils.py,sha256=XJ6ZZBR5hols19TcX4Bh49ygSNhPt3MLncLR-g41GTA,6858
|
|
@@ -133,13 +133,13 @@ port_ocean/exceptions/api.py,sha256=1JcA-H12lhSgolMEA6dM4JMbDrh9sYDcE7oydPSTuK8,
|
|
|
133
133
|
port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
|
|
134
134
|
port_ocean/exceptions/clients.py,sha256=LKLLs-Zy3caNG85rwxfOw2rMr8qqVV6SHUq4fRCZ99U,180
|
|
135
135
|
port_ocean/exceptions/context.py,sha256=mA8HII6Rl4QxKUz98ppy1zX3kaziaen21h1ZWuU3ADc,372
|
|
136
|
-
port_ocean/exceptions/core.py,sha256=
|
|
136
|
+
port_ocean/exceptions/core.py,sha256=3LpQrOWdZ-xZ8zm90DmTnFnk0Nms2OgrVIzZBK0Xw5M,931
|
|
137
137
|
port_ocean/exceptions/port_defaults.py,sha256=2a7Koy541KxMan33mU-gbauUxsumG3NT4itVxSpQqfw,666
|
|
138
138
|
port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnqI,197
|
|
139
139
|
port_ocean/exceptions/webhook_processor.py,sha256=yQYazg53Y-ohb7HfViwq1opH_ZUuUdhHSRxcUNveFpI,114
|
|
140
140
|
port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
141
141
|
port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861vE3K40,1783
|
|
142
|
-
port_ocean/helpers/metric/metric.py,sha256=
|
|
142
|
+
port_ocean/helpers/metric/metric.py,sha256=akRZmjzQdY1WMY2O3pDuU0xyW_Tn3XGttG5CkT9_Cbo,14503
|
|
143
143
|
port_ocean/helpers/metric/utils.py,sha256=1lAgrxnZLuR_wUNDyPOPzLrm32b8cDdioob2lvnPQ1A,1619
|
|
144
144
|
port_ocean/helpers/retry.py,sha256=gmS4YxM6N4fboFp7GSgtOzyBJemxs46bnrz4L4rDS6Y,16136
|
|
145
145
|
port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -158,15 +158,15 @@ port_ocean/tests/cache/test_memory_cache.py,sha256=xlwIOBU0RVLYYJU83l_aoZDzZ6QID
|
|
|
158
158
|
port_ocean/tests/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
159
159
|
port_ocean/tests/clients/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
160
160
|
port_ocean/tests/clients/oauth/test_oauth_client.py,sha256=2XVMQUalDpiD539Z7_dk5BK_ngXQzsTmb2lNBsfEm9c,3266
|
|
161
|
-
port_ocean/tests/clients/port/mixins/test_entities.py,sha256=
|
|
161
|
+
port_ocean/tests/clients/port/mixins/test_entities.py,sha256=DeWbAQcaxT3LQQf_j9HA5nG7YgsQDvXmgK2aghlG9ug,6619
|
|
162
162
|
port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=zzKYz3h8dl4Z5A2QG_924m0y9U6XTth1XYOfwNrd_24,914
|
|
163
163
|
port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
|
|
164
|
-
port_ocean/tests/core/conftest.py,sha256=
|
|
164
|
+
port_ocean/tests/core/conftest.py,sha256=7K_M1--wQ08VmiQRB0vo1nst2X00cwsuBS5UfERsnG8,7589
|
|
165
165
|
port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
|
|
166
|
-
port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=
|
|
166
|
+
port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=eJYXc7AwrV0XRS6HpixwzghjB3pspT5Gxr9twvJE7fk,8290
|
|
167
167
|
port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=8WpMn559Mf0TFWmloRpZrVgr6yWwyA0C4n2lVHCtyq4,13596
|
|
168
168
|
port_ocean/tests/core/handlers/mixins/test_live_events.py,sha256=iAwVpr3n3PIkXQLw7hxd-iB_SR_vyfletVXJLOmyz28,12480
|
|
169
|
-
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=
|
|
169
|
+
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=E_r_L6Jr6DTPyUP5fn3yQ-MAC3YbpO2AHf690NyLW4g,44447
|
|
170
170
|
port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
|
|
171
171
|
port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=hSh556bJM9zuELwhwnyKSfd9z06WqWXIfe-6hCl5iKI,9799
|
|
172
172
|
port_ocean/tests/core/handlers/queue/test_local_queue.py,sha256=9Ly0HzZXbs6Rbl_bstsIdInC3h2bgABU3roP9S_PnJM,2582
|
|
@@ -183,7 +183,7 @@ port_ocean/tests/helpers/integration.py,sha256=_RxS-RHpu11lrbhUXYPZp862HLWx8AoD7
|
|
|
183
183
|
port_ocean/tests/helpers/ocean_app.py,sha256=N06vcNI1klqdcNFq-PXL5vm77u-hODsOSXnj9p8d1AI,2249
|
|
184
184
|
port_ocean/tests/helpers/port_client.py,sha256=5d6GNr8vNNSOkrz1AdOhxBUKuusr_-UPDP7AVpHasQw,599
|
|
185
185
|
port_ocean/tests/helpers/smoke_test.py,sha256=_9aJJFRfuGJEg2D2YQJVJRmpreS6gEPHHQq8Q01x4aQ,2697
|
|
186
|
-
port_ocean/tests/log/test_handlers.py,sha256=
|
|
186
|
+
port_ocean/tests/log/test_handlers.py,sha256=x2P2Hd6Cb3sQafIE3TRGltbbHeiFHaiEjwRn9py_03g,2165
|
|
187
187
|
port_ocean/tests/test_metric.py,sha256=gDdeJcqJDQ_o3VrYrW23iZyw2NuUsyATdrygSXhcDuQ,8096
|
|
188
188
|
port_ocean/tests/test_ocean.py,sha256=bsXKGTVEjwLSbR7-qSmI4GZ-EzDo0eBE3TNSMsWzYxM,1502
|
|
189
189
|
port_ocean/tests/test_smoke.py,sha256=uix2uIg_yOm8BHDgHw2hTFPy1fiIyxBGW3ENU_KoFlo,2557
|
|
@@ -200,8 +200,8 @@ port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,32
|
|
|
200
200
|
port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
|
|
201
201
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
|
202
202
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
|
203
|
-
port_ocean-0.
|
|
204
|
-
port_ocean-0.
|
|
205
|
-
port_ocean-0.
|
|
206
|
-
port_ocean-0.
|
|
207
|
-
port_ocean-0.
|
|
203
|
+
port_ocean-0.24.0.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
204
|
+
port_ocean-0.24.0.dist-info/METADATA,sha256=I_Ne_n_R7YjRUyEUR74UJ0u7b5NToTapBpVT0TzFbqU,6764
|
|
205
|
+
port_ocean-0.24.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
206
|
+
port_ocean-0.24.0.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
|
207
|
+
port_ocean-0.24.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|