port-ocean 0.22.11__py3-none-any.whl → 0.23.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.local +41 -0
- integrations/_infra/entry_local.sh +27 -0
- port_ocean/__init__.py +6 -1
- port_ocean/cache/__init__.py +0 -0
- port_ocean/cache/base.py +25 -0
- port_ocean/cache/disk.py +61 -0
- port_ocean/cache/errors.py +10 -0
- port_ocean/cache/memory.py +36 -0
- port_ocean/config/settings.py +9 -2
- port_ocean/core/integrations/mixins/sync_raw.py +83 -27
- port_ocean/core/integrations/mixins/utils.py +3 -3
- port_ocean/core/models.py +10 -0
- port_ocean/helpers/metric/metric.py +4 -0
- port_ocean/ocean.py +28 -1
- port_ocean/run.py +0 -1
- port_ocean/tests/cache/__init__.py +1 -0
- port_ocean/tests/cache/test_disk_cache.py +92 -0
- port_ocean/tests/cache/test_memory_cache.py +59 -0
- port_ocean/tests/core/conftest.py +13 -3
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +2 -28
- port_ocean/tests/utils/test_cache.py +110 -25
- port_ocean/utils/cache.py +35 -12
- port_ocean/utils/ipc.py +30 -0
- {port_ocean-0.22.11.dist-info → port_ocean-0.23.0.dist-info}/METADATA +1 -1
- {port_ocean-0.22.11.dist-info → port_ocean-0.23.0.dist-info}/RECORD +28 -17
- {port_ocean-0.22.11.dist-info → port_ocean-0.23.0.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.22.11.dist-info → port_ocean-0.23.0.dist-info}/WHEEL +0 -0
- {port_ocean-0.22.11.dist-info → port_ocean-0.23.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
ARG BASE_PYTHON_IMAGE=debian:trixie-slim
|
|
2
|
+
# debian:trixie-slim - Python 3.12
|
|
3
|
+
FROM ${BASE_PYTHON_IMAGE}
|
|
4
|
+
|
|
5
|
+
RUN apt-get update \
|
|
6
|
+
&& apt-get install -y --no-install-recommends librdkafka-dev python3 \
|
|
7
|
+
&& apt-get clean
|
|
8
|
+
RUN apt-get update \
|
|
9
|
+
&& apt-get install -y \
|
|
10
|
+
--no-install-recommends \
|
|
11
|
+
wget \
|
|
12
|
+
g++ \
|
|
13
|
+
libssl-dev \
|
|
14
|
+
autoconf \
|
|
15
|
+
automake \
|
|
16
|
+
libtool \
|
|
17
|
+
curl \
|
|
18
|
+
librdkafka-dev \
|
|
19
|
+
python3 \
|
|
20
|
+
python3-pip \
|
|
21
|
+
python3-poetry \
|
|
22
|
+
build-essential\
|
|
23
|
+
git \
|
|
24
|
+
python3-venv \
|
|
25
|
+
&& apt-get clean
|
|
26
|
+
|
|
27
|
+
ARG BUILD_CONTEXT
|
|
28
|
+
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
|
|
31
|
+
COPY . .
|
|
32
|
+
RUN rm -rf .venv-docker ${BUILD_CONTEXT}/.venv-docker
|
|
33
|
+
RUN python3 -m venv .venv-docker
|
|
34
|
+
RUN python3 -m venv ${BUILD_CONTEXT}/.venv-docker
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
WORKDIR /app/${BUILD_CONTEXT}
|
|
38
|
+
|
|
39
|
+
WORKDIR /app
|
|
40
|
+
|
|
41
|
+
ENTRYPOINT ["./integrations/_infra/entry_local.sh"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
mkdir -p /tmp/prometheus_multiproc_dir
|
|
3
|
+
export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc_dir
|
|
4
|
+
if [ -z "$BUILD_CONTEXT" ]; then
|
|
5
|
+
echo "BUILD_CONTEXT is not set"
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
if [ ! -d ".venv-docker" ]; then
|
|
10
|
+
/usr/bin/python3 -m venv .venv-docker
|
|
11
|
+
source .venv-docker/bin/activate
|
|
12
|
+
python -m pip install poetry
|
|
13
|
+
python -m poetry install
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
cd $BUILD_CONTEXT
|
|
17
|
+
|
|
18
|
+
if [ ! -d ".venv-docker" ]; then
|
|
19
|
+
/usr/bin/python3 -m venv .venv-docker
|
|
20
|
+
source .venv-docker/bin/activate
|
|
21
|
+
python -m pip install poetry
|
|
22
|
+
python -m poetry install
|
|
23
|
+
fi
|
|
24
|
+
source .venv-docker/bin/activate
|
|
25
|
+
python -m pip install -e ../../
|
|
26
|
+
|
|
27
|
+
ocean sail
|
port_ocean/__init__.py
CHANGED
|
@@ -8,4 +8,9 @@ from .run import run # noqa: E402
|
|
|
8
8
|
from .version import __integration_version__, __version__ # noqa: E402
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
__all__ = [
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Ocean",
|
|
13
|
+
"run",
|
|
14
|
+
"__version__",
|
|
15
|
+
"__integration_version__",
|
|
16
|
+
]
|
|
File without changes
|
port_ocean/cache/base.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from port_ocean.core.models import CachingStorageMode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CacheProvider(ABC):
|
|
8
|
+
"""Base class for cache providers that defines the contract for all cache implementations."""
|
|
9
|
+
|
|
10
|
+
STORAGE_TYPE: CachingStorageMode
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def get(self, key: str) -> Optional[Any]:
|
|
14
|
+
"""Get a value from the cache."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def set(self, key: str, value: Any) -> None:
|
|
19
|
+
"""Set a value in the cache."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def clear(self) -> None:
|
|
24
|
+
"""Clear all values from the cache."""
|
|
25
|
+
pass
|
port_ocean/cache/disk.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from port_ocean.cache.base import CacheProvider
|
|
6
|
+
from port_ocean.cache.errors import FailedToReadCacheError, FailedToWriteCacheError
|
|
7
|
+
from port_ocean.core.models import CachingStorageMode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FailedToReadCacheFileError(FailedToReadCacheError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FailedToWriteCacheFileError(FailedToWriteCacheError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DiskCacheProvider(CacheProvider):
|
|
19
|
+
STORAGE_TYPE = CachingStorageMode.disk
|
|
20
|
+
|
|
21
|
+
def __init__(self, cache_dir: str | None = None) -> None:
|
|
22
|
+
if cache_dir is None:
|
|
23
|
+
cache_dir = ".ocean_cache"
|
|
24
|
+
self._cache_dir = Path(cache_dir)
|
|
25
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
def _get_cache_path(self, key: str) -> Path:
|
|
28
|
+
return self._cache_dir / f"{key}.pkl"
|
|
29
|
+
|
|
30
|
+
async def get(self, key: str) -> Optional[Any]:
|
|
31
|
+
cache_path = self._get_cache_path(key)
|
|
32
|
+
if not cache_path.exists():
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(cache_path, "rb") as f:
|
|
37
|
+
return pickle.load(f)
|
|
38
|
+
except (pickle.PickleError, EOFError) as e:
|
|
39
|
+
raise FailedToReadCacheFileError(
|
|
40
|
+
f"Failed to read cache file: {cache_path}: {str(e)}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def set(self, key: str, value: Any) -> None:
|
|
44
|
+
cache_path = self._get_cache_path(key)
|
|
45
|
+
try:
|
|
46
|
+
with open(cache_path, "wb") as f:
|
|
47
|
+
pickle.dump(value, f)
|
|
48
|
+
except (pickle.PickleError, IOError) as e:
|
|
49
|
+
raise FailedToWriteCacheFileError(
|
|
50
|
+
f"Failed to write cache file: {cache_path}: {str(e)}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def clear(self) -> None:
|
|
54
|
+
try:
|
|
55
|
+
for cache_file in self._cache_dir.glob("*.pkl"):
|
|
56
|
+
try:
|
|
57
|
+
cache_file.unlink()
|
|
58
|
+
except OSError:
|
|
59
|
+
pass
|
|
60
|
+
except OSError:
|
|
61
|
+
pass
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
from port_ocean.cache.base import CacheProvider
|
|
3
|
+
from port_ocean.cache.errors import FailedToReadCacheError, FailedToWriteCacheError
|
|
4
|
+
from port_ocean.core.models import CachingStorageMode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FailedToReadCacheMemoryError(FailedToReadCacheError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FailedToWriteCacheMemoryError(FailedToWriteCacheError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InMemoryCacheProvider(CacheProvider):
|
|
16
|
+
CACHE_KEY = "cache"
|
|
17
|
+
STORAGE_TYPE = CachingStorageMode.memory
|
|
18
|
+
|
|
19
|
+
def __init__(self, caching_storage: dict[str, Any] | None = None) -> None:
|
|
20
|
+
self._storage = caching_storage or {}
|
|
21
|
+
self._storage[self.CACHE_KEY] = self._storage.get(self.CACHE_KEY, {})
|
|
22
|
+
|
|
23
|
+
async def get(self, key: str) -> Optional[Any]:
|
|
24
|
+
try:
|
|
25
|
+
return self._storage.get(self.CACHE_KEY, {}).get(key)
|
|
26
|
+
except KeyError as e:
|
|
27
|
+
raise FailedToReadCacheMemoryError(f"Failed to read cache: {str(e)}")
|
|
28
|
+
|
|
29
|
+
async def set(self, key: str, value: Any) -> None:
|
|
30
|
+
try:
|
|
31
|
+
self._storage[self.CACHE_KEY][key] = value
|
|
32
|
+
except KeyError as e:
|
|
33
|
+
raise FailedToWriteCacheMemoryError(f"Failed to write cache: {str(e)}")
|
|
34
|
+
|
|
35
|
+
async def clear(self) -> None:
|
|
36
|
+
self._storage[self.CACHE_KEY].clear()
|
port_ocean/config/settings.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Literal, Type, cast
|
|
1
|
+
from typing import Any, Literal, Optional, Type, cast
|
|
2
2
|
|
|
3
3
|
from pydantic import AnyHttpUrl, Extra, parse_obj_as, parse_raw_as
|
|
4
4
|
from pydantic.class_validators import root_validator, validator
|
|
@@ -8,7 +8,12 @@ from pydantic.main import BaseModel
|
|
|
8
8
|
|
|
9
9
|
from port_ocean.config.base import BaseOceanModel, BaseOceanSettings
|
|
10
10
|
from port_ocean.core.event_listener import EventListenerSettingsType
|
|
11
|
-
from port_ocean.core.models import
|
|
11
|
+
from port_ocean.core.models import (
|
|
12
|
+
CachingStorageMode,
|
|
13
|
+
CreatePortResourcesOrigin,
|
|
14
|
+
Runtime,
|
|
15
|
+
ProcessExecutionMode,
|
|
16
|
+
)
|
|
12
17
|
from port_ocean.utils.misc import get_integration_name, get_spec_file
|
|
13
18
|
|
|
14
19
|
LogLevelType = Literal["ERROR", "WARNING", "INFO", "DEBUG", "CRITICAL"]
|
|
@@ -93,6 +98,8 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
|
|
|
93
98
|
)
|
|
94
99
|
max_event_processing_seconds: float = 90.0
|
|
95
100
|
max_wait_seconds_before_shutdown: float = 5.0
|
|
101
|
+
caching_storage_mode: Optional[CachingStorageMode] = Field(default=None)
|
|
102
|
+
process_execution_mode: Optional[ProcessExecutionMode] = Field(default=None)
|
|
96
103
|
|
|
97
104
|
@validator("metrics", pre=True)
|
|
98
105
|
def validate_metrics(cls, v: Any) -> MetricsSettings | dict[str, Any] | None:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import uuid
|
|
2
3
|
from graphlib import CycleError
|
|
3
4
|
import inspect
|
|
4
5
|
import typing
|
|
5
6
|
from typing import Callable, Awaitable, Any
|
|
6
|
-
|
|
7
|
+
import multiprocessing
|
|
7
8
|
import httpx
|
|
8
9
|
from loguru import logger
|
|
9
|
-
|
|
10
10
|
from port_ocean.clients.port.types import UserAgentType
|
|
11
11
|
from port_ocean.context.event import TriggerType, event_context, EventType, event
|
|
12
12
|
from port_ocean.context.ocean import ocean
|
|
@@ -20,7 +20,7 @@ from port_ocean.core.integrations.mixins.utils import (
|
|
|
20
20
|
resync_generator_wrapper,
|
|
21
21
|
resync_function_wrapper,
|
|
22
22
|
)
|
|
23
|
-
from port_ocean.core.models import Entity
|
|
23
|
+
from port_ocean.core.models import Entity, ProcessExecutionMode
|
|
24
24
|
from port_ocean.core.ocean_types import (
|
|
25
25
|
RAW_RESULT,
|
|
26
26
|
RESYNC_RESULT,
|
|
@@ -33,6 +33,7 @@ from port_ocean.core.utils.utils import resolve_entities_diff, zip_and_sum, gath
|
|
|
33
33
|
from port_ocean.exceptions.core import OceanAbortException
|
|
34
34
|
from port_ocean.helpers.metric.metric import SyncState, MetricType, MetricPhase
|
|
35
35
|
from port_ocean.helpers.metric.utils import TimeMetric
|
|
36
|
+
from port_ocean.utils.ipc import FileIPC
|
|
36
37
|
|
|
37
38
|
SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
|
|
38
39
|
|
|
@@ -67,6 +68,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
67
68
|
)
|
|
68
69
|
|
|
69
70
|
fns = self._collect_resync_functions(resource_config)
|
|
71
|
+
logger.info(f"Found {len(fns)} resync functions for {resource_config.kind}")
|
|
70
72
|
|
|
71
73
|
results, errors = await self._execute_resync_tasks(fns, resource_config)
|
|
72
74
|
|
|
@@ -96,8 +98,10 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
96
98
|
results = []
|
|
97
99
|
for task in fns:
|
|
98
100
|
if inspect.isasyncgenfunction(task):
|
|
101
|
+
logger.info(f"Found async generator function for {resource_config.kind} name: {task.__qualname__}")
|
|
99
102
|
results.append(resync_generator_wrapper(task, resource_config.kind))
|
|
100
103
|
else:
|
|
104
|
+
logger.info(f"Found sync function for {resource_config.kind} name: {task.__qualname__}")
|
|
101
105
|
task = typing.cast(Callable[[str], Awaitable[RAW_RESULT]], task)
|
|
102
106
|
tasks.append(resync_function_wrapper(task, resource_config.kind))
|
|
103
107
|
|
|
@@ -264,7 +268,6 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
264
268
|
objects_diff[0].entity_selector_diff.passed, user_agent_type
|
|
265
269
|
)
|
|
266
270
|
|
|
267
|
-
|
|
268
271
|
return CalculationResult(
|
|
269
272
|
number_of_transformed_entities=len(objects_diff[0].entity_selector_diff.passed),
|
|
270
273
|
entity_selector_diff=objects_diff[0].entity_selector_diff._replace(passed=modified_objects),
|
|
@@ -341,8 +344,8 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
341
344
|
user_agent_type,
|
|
342
345
|
send_raw_data_examples_amount=send_raw_data_examples_amount
|
|
343
346
|
)
|
|
344
|
-
errors.extend(calculation_result.errors)
|
|
345
347
|
passed_entities.extend(calculation_result.entity_selector_diff.passed)
|
|
348
|
+
errors.extend(calculation_result.errors)
|
|
346
349
|
number_of_transformed_entities += calculation_result.number_of_transformed_entities
|
|
347
350
|
except* OceanAbortException as error:
|
|
348
351
|
ocean.metrics.sync_state = SyncState.FAILED
|
|
@@ -352,6 +355,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
352
355
|
f"Finished registering kind: {resource_config.kind}-{resource.resource.index} ,{len(passed_entities)} entities out of {number_of_raw_results} raw results"
|
|
353
356
|
)
|
|
354
357
|
|
|
358
|
+
|
|
355
359
|
ocean.metrics.set_metric(
|
|
356
360
|
name=MetricType.SUCCESS_NAME,
|
|
357
361
|
labels=[ocean.metrics.current_resource_kind(), MetricPhase.RESYNC],
|
|
@@ -581,6 +585,72 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
581
585
|
for entity in event.entity_topological_sorter.get_entities(False):
|
|
582
586
|
await self.entities_state_applier.context.port_client.upsert_entity(entity,event.port_app_config.get_port_request_options(),user_agent_type,should_raise=False)
|
|
583
587
|
|
|
588
|
+
def process_resource_in_subprocess(self,
|
|
589
|
+
file_ipc_map: dict[str, FileIPC],
|
|
590
|
+
resource: ResourceConfig,
|
|
591
|
+
index: int,
|
|
592
|
+
user_agent_type: UserAgentType,
|
|
593
|
+
) -> None:
|
|
594
|
+
logger.info(f"process started successfully for {resource.kind} with index {index}")
|
|
595
|
+
|
|
596
|
+
async def process_resource_task() -> None:
|
|
597
|
+
result = await self._process_resource(
|
|
598
|
+
resource, index, user_agent_type
|
|
599
|
+
)
|
|
600
|
+
file_ipc_map["process_resource"].save(result)
|
|
601
|
+
file_ipc_map["topological_entities"].save(
|
|
602
|
+
event.entity_topological_sorter.entities
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
asyncio.run(process_resource_task())
|
|
606
|
+
logger.info(f"Process finished for {resource.kind} with index {index}")
|
|
607
|
+
|
|
608
|
+
async def process_resource(self, resource: ResourceConfig, index: int, user_agent_type: UserAgentType) -> tuple[list[Entity], list[Exception]]:
|
|
609
|
+
if ocean.app.process_execution_mode == ProcessExecutionMode.multi_process:
|
|
610
|
+
id = uuid.uuid4()
|
|
611
|
+
logger.info(f"Starting subprocess with id {id}")
|
|
612
|
+
file_ipc_map = {
|
|
613
|
+
"process_resource": FileIPC(id, "process_resource",([],[])),
|
|
614
|
+
"topological_entities": FileIPC(id, "topological_entities",[]),
|
|
615
|
+
}
|
|
616
|
+
process = multiprocessing.Process(target=self.process_resource_in_subprocess, args=(file_ipc_map,resource,index,user_agent_type))
|
|
617
|
+
process.start()
|
|
618
|
+
process.join()
|
|
619
|
+
if process.exitcode != 0:
|
|
620
|
+
logger.error(f"Process {id} failed with exit code {process.exitcode}")
|
|
621
|
+
event.entity_topological_sorter.entities.extend(file_ipc_map["topological_entities"].load())
|
|
622
|
+
return file_ipc_map["process_resource"].load()
|
|
623
|
+
|
|
624
|
+
else:
|
|
625
|
+
return await self._process_resource(resource,index,user_agent_type)
|
|
626
|
+
|
|
627
|
+
async def _process_resource(self,resource: ResourceConfig, index: int, user_agent_type: UserAgentType)-> tuple[list[Entity], list[Exception]]:
|
|
628
|
+
# create resource context per resource kind, so resync method could have access to the resource
|
|
629
|
+
# config as we might have multiple resources in the same event
|
|
630
|
+
async with resource_context(resource,index):
|
|
631
|
+
resource_kind_id = f"{resource.kind}-{index}"
|
|
632
|
+
ocean.metrics.sync_state = SyncState.SYNCING
|
|
633
|
+
task = asyncio.create_task(
|
|
634
|
+
self._register_in_batches(resource, user_agent_type)
|
|
635
|
+
)
|
|
636
|
+
event.on_abort(lambda: task.cancel())
|
|
637
|
+
kind_results: tuple[list[Entity], list[Exception]] = await task
|
|
638
|
+
ocean.metrics.set_metric(
|
|
639
|
+
name=MetricType.OBJECT_COUNT_NAME,
|
|
640
|
+
labels=[ocean.metrics.current_resource_kind(), MetricPhase.LOAD, MetricPhase.LoadResult.LOADED],
|
|
641
|
+
value=len(kind_results[0])
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
if ocean.metrics.sync_state != SyncState.FAILED:
|
|
645
|
+
ocean.metrics.sync_state = SyncState.COMPLETED
|
|
646
|
+
|
|
647
|
+
await ocean.metrics.send_metrics_to_webhook(
|
|
648
|
+
kind=resource_kind_id
|
|
649
|
+
)
|
|
650
|
+
# await ocean.metrics.report_kind_sync_metrics(kind=resource_kind_id) # TODO: uncomment this when end points are ready
|
|
651
|
+
|
|
652
|
+
return kind_results
|
|
653
|
+
|
|
584
654
|
@TimeMetric(MetricPhase.RESYNC)
|
|
585
655
|
async def sync_raw_all(
|
|
586
656
|
self,
|
|
@@ -619,6 +689,9 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
619
689
|
ocean.metrics.initialize_metrics(kinds)
|
|
620
690
|
# await ocean.metrics.report_sync_metrics(kinds=kinds) # TODO: uncomment this when end points are ready
|
|
621
691
|
|
|
692
|
+
# Clear cache
|
|
693
|
+
await ocean.app.cache_provider.clear()
|
|
694
|
+
|
|
622
695
|
# Execute resync_start hooks
|
|
623
696
|
for resync_start_fn in self.event_strategy["resync_start"]:
|
|
624
697
|
await resync_start_fn()
|
|
@@ -637,32 +710,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
637
710
|
|
|
638
711
|
creation_results: list[tuple[list[Entity], list[Exception]]] = []
|
|
639
712
|
|
|
640
|
-
|
|
713
|
+
multiprocessing.set_start_method('fork', True)
|
|
641
714
|
try:
|
|
642
715
|
for index,resource in enumerate(app_config.resources):
|
|
643
|
-
# create resource context per resource kind, so resync method could have access to the resource
|
|
644
|
-
# config as we might have multiple resources in the same event
|
|
645
|
-
async with resource_context(resource,index):
|
|
646
|
-
resource_kind_id = f"{resource.kind}-{index}"
|
|
647
|
-
ocean.metrics.sync_state = SyncState.SYNCING
|
|
648
|
-
# await ocean.metrics.report_kind_sync_metrics(kind=resource_kind_id) # TODO: uncomment this when end points are ready
|
|
649
|
-
|
|
650
|
-
task = asyncio.create_task(
|
|
651
|
-
self._register_in_batches(resource, user_agent_type)
|
|
652
|
-
)
|
|
653
716
|
|
|
654
|
-
|
|
655
|
-
kind_results: tuple[list[Entity], list[Exception]] = await task
|
|
717
|
+
logger.info(f"Starting processing resource {resource.kind} with index {index}")
|
|
656
718
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if ocean.metrics.sync_state != SyncState.FAILED:
|
|
660
|
-
ocean.metrics.sync_state = SyncState.COMPLETED
|
|
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
|
|
719
|
+
creation_results.append(await self.process_resource(resource,index,user_agent_type))
|
|
666
720
|
|
|
667
721
|
await self.sort_and_upsert_failed_entities(user_agent_type)
|
|
668
722
|
|
|
@@ -718,3 +772,5 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
|
718
772
|
logger.info("Finished executing resync_complete hooks")
|
|
719
773
|
|
|
720
774
|
return True
|
|
775
|
+
finally:
|
|
776
|
+
await ocean.app.cache_provider.clear()
|
|
@@ -22,9 +22,9 @@ def resync_error_handling() -> Generator[None, None, None]:
|
|
|
22
22
|
try:
|
|
23
23
|
yield
|
|
24
24
|
except RawObjectValidationException as error:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
) from error
|
|
25
|
+
err_msg = f"Failed to validate raw data for returned data from resync function, error: {error}"
|
|
26
|
+
logger.exception(err_msg)
|
|
27
|
+
raise OceanAbortException(err_msg) from error
|
|
28
28
|
except StopAsyncIteration:
|
|
29
29
|
raise
|
|
30
30
|
except Exception as error:
|
port_ocean/core/models.py
CHANGED
|
@@ -11,6 +11,16 @@ class CreatePortResourcesOrigin(StrEnum):
|
|
|
11
11
|
Port = "Port"
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class ProcessExecutionMode(StrEnum):
|
|
15
|
+
multi_process = "multi_process"
|
|
16
|
+
single_process = "single_process"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CachingStorageMode(StrEnum):
|
|
20
|
+
disk = "disk"
|
|
21
|
+
memory = "memory"
|
|
22
|
+
|
|
23
|
+
|
|
14
24
|
class Runtime(Enum):
|
|
15
25
|
Saas = "Saas"
|
|
16
26
|
OnPrem = "OnPrem"
|
|
@@ -10,6 +10,7 @@ from prometheus_client import Gauge
|
|
|
10
10
|
import prometheus_client.openmetrics
|
|
11
11
|
import prometheus_client.openmetrics.exposition
|
|
12
12
|
import prometheus_client.parser
|
|
13
|
+
from prometheus_client import multiprocess
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from port_ocean.config.settings import MetricsSettings, IntegrationSettings
|
|
@@ -108,11 +109,14 @@ class Metrics:
|
|
|
108
109
|
metrics_settings: "MetricsSettings",
|
|
109
110
|
integration_configuration: "IntegrationSettings",
|
|
110
111
|
port_client: "PortClient",
|
|
112
|
+
multiprocessing_enabled: bool = False,
|
|
111
113
|
) -> None:
|
|
112
114
|
self.metrics_settings = metrics_settings
|
|
113
115
|
self.integration_configuration = integration_configuration
|
|
114
116
|
self.port_client = port_client
|
|
115
117
|
self.registry = prometheus_client.CollectorRegistry()
|
|
118
|
+
if multiprocessing_enabled:
|
|
119
|
+
multiprocess.MultiProcessCollector(self.registry)
|
|
116
120
|
self.metrics: dict[str, Gauge] = {}
|
|
117
121
|
self.load_metrics()
|
|
118
122
|
self._integration_version: Optional[str] = None
|
port_ocean/ocean.py
CHANGED
|
@@ -4,6 +4,10 @@ from contextlib import asynccontextmanager
|
|
|
4
4
|
import threading
|
|
5
5
|
from typing import Any, AsyncIterator, Callable, Dict, Type
|
|
6
6
|
|
|
7
|
+
from port_ocean.cache.base import CacheProvider
|
|
8
|
+
from port_ocean.cache.disk import DiskCacheProvider
|
|
9
|
+
from port_ocean.cache.memory import InMemoryCacheProvider
|
|
10
|
+
from port_ocean.core.models import ProcessExecutionMode
|
|
7
11
|
import port_ocean.helpers.metric.metric
|
|
8
12
|
|
|
9
13
|
from fastapi import FastAPI, APIRouter
|
|
@@ -66,11 +70,16 @@ class Ocean:
|
|
|
66
70
|
integration_type=self.config.integration.type,
|
|
67
71
|
integration_version=__integration_version__,
|
|
68
72
|
)
|
|
69
|
-
|
|
73
|
+
self.cache_provider: CacheProvider = self._get_caching_provider()
|
|
74
|
+
self.process_execution_mode: ProcessExecutionMode = (
|
|
75
|
+
self._get_process_execution_mode()
|
|
76
|
+
)
|
|
70
77
|
self.metrics = port_ocean.helpers.metric.metric.Metrics(
|
|
71
78
|
metrics_settings=self.config.metrics,
|
|
72
79
|
integration_configuration=self.config.integration,
|
|
73
80
|
port_client=self.port_client,
|
|
81
|
+
multiprocessing_enabled=self.process_execution_mode
|
|
82
|
+
== ProcessExecutionMode.multi_process,
|
|
74
83
|
)
|
|
75
84
|
|
|
76
85
|
self.webhook_manager = LiveEventsProcessorManager(
|
|
@@ -90,6 +99,24 @@ class Ocean:
|
|
|
90
99
|
|
|
91
100
|
self.app_initialized = False
|
|
92
101
|
|
|
102
|
+
def _get_process_execution_mode(self) -> ProcessExecutionMode:
|
|
103
|
+
if self.config.process_execution_mode:
|
|
104
|
+
return self.config.process_execution_mode
|
|
105
|
+
return ProcessExecutionMode.single_process
|
|
106
|
+
|
|
107
|
+
def _get_caching_provider(self) -> CacheProvider:
|
|
108
|
+
if self.config.caching_storage_mode:
|
|
109
|
+
caching_type_to_provider = {
|
|
110
|
+
DiskCacheProvider.STORAGE_TYPE: DiskCacheProvider,
|
|
111
|
+
InMemoryCacheProvider.STORAGE_TYPE: InMemoryCacheProvider,
|
|
112
|
+
}
|
|
113
|
+
if self.config.caching_storage_mode in caching_type_to_provider:
|
|
114
|
+
return caching_type_to_provider[self.config.caching_storage_mode]()
|
|
115
|
+
|
|
116
|
+
if self.config.process_execution_mode == ProcessExecutionMode.multi_process:
|
|
117
|
+
return DiskCacheProvider()
|
|
118
|
+
return InMemoryCacheProvider()
|
|
119
|
+
|
|
93
120
|
def is_saas(self) -> bool:
|
|
94
121
|
return self.config.runtime.is_saas_runtime
|
|
95
122
|
|
port_ocean/run.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for cache providers."""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from port_ocean.cache.disk import (
|
|
6
|
+
DiskCacheProvider,
|
|
7
|
+
FailedToReadCacheFileError,
|
|
8
|
+
FailedToWriteCacheFileError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def disk_cache(tmp_path: Path) -> DiskCacheProvider:
|
|
14
|
+
"""Fixture that provides a DiskCacheProvider with a temporary directory."""
|
|
15
|
+
return DiskCacheProvider(cache_dir=str(tmp_path))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_disk_cache_set_get(disk_cache: DiskCacheProvider) -> None:
|
|
20
|
+
"""Test setting and getting values from disk cache."""
|
|
21
|
+
# Test basic set/get
|
|
22
|
+
await disk_cache.set("test_key", "test_value")
|
|
23
|
+
assert await disk_cache.get("test_key") == "test_value"
|
|
24
|
+
|
|
25
|
+
# Test with different types
|
|
26
|
+
test_data = {
|
|
27
|
+
"string": "hello",
|
|
28
|
+
"int": 42,
|
|
29
|
+
"float": 3.14,
|
|
30
|
+
"list": [1, 2, 3],
|
|
31
|
+
"dict": {"a": 1, "b": 2},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for key, value in test_data.items():
|
|
35
|
+
await disk_cache.set(key, value)
|
|
36
|
+
assert await disk_cache.get(key) == value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_disk_cache_clear(disk_cache: DiskCacheProvider) -> None:
|
|
41
|
+
"""Test clearing all values from disk cache."""
|
|
42
|
+
# Add multiple values
|
|
43
|
+
for i in range(5):
|
|
44
|
+
await disk_cache.set(f"key_{i}", f"value_{i}")
|
|
45
|
+
|
|
46
|
+
# Verify values exist
|
|
47
|
+
for i in range(5):
|
|
48
|
+
assert await disk_cache.get(f"key_{i}") == f"value_{i}"
|
|
49
|
+
|
|
50
|
+
# Clear cache
|
|
51
|
+
await disk_cache.clear()
|
|
52
|
+
|
|
53
|
+
# Verify all values are gone
|
|
54
|
+
for i in range(5):
|
|
55
|
+
assert await disk_cache.get(f"key_{i}") is None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_disk_cache_nonexistent_key(disk_cache: DiskCacheProvider) -> None:
|
|
60
|
+
"""Test getting a nonexistent key from disk cache."""
|
|
61
|
+
assert await disk_cache.get("nonexistent_key") is None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
async def test_disk_cache_corrupted_file(
|
|
66
|
+
disk_cache: DiskCacheProvider, tmp_path: Path
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Test handling of corrupted cache files."""
|
|
69
|
+
# Create a corrupted pickle file
|
|
70
|
+
cache_path = tmp_path / "test_key.pkl"
|
|
71
|
+
with open(cache_path, "wb") as f:
|
|
72
|
+
f.write(b"invalid pickle data")
|
|
73
|
+
|
|
74
|
+
# Attempting to read should raise FailedToReadCacheFileError
|
|
75
|
+
with pytest.raises(FailedToReadCacheFileError):
|
|
76
|
+
await disk_cache.get("test_key")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_disk_cache_write_error(
|
|
81
|
+
disk_cache: DiskCacheProvider, tmp_path: Path
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Test handling of write errors."""
|
|
84
|
+
# Make the cache directory read-only
|
|
85
|
+
os.chmod(tmp_path, 0o444)
|
|
86
|
+
|
|
87
|
+
# Attempting to write should raise FailedToWriteCacheFileError
|
|
88
|
+
with pytest.raises(FailedToWriteCacheFileError):
|
|
89
|
+
await disk_cache.set("test_key", "test_value")
|
|
90
|
+
|
|
91
|
+
# Restore permissions
|
|
92
|
+
os.chmod(tmp_path, 0o755)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from port_ocean.cache.memory import (
|
|
4
|
+
InMemoryCacheProvider,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def memory_cache() -> InMemoryCacheProvider:
|
|
10
|
+
"""Fixture that provides an InMemoryCacheProvider."""
|
|
11
|
+
return InMemoryCacheProvider()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_memory_cache_set_get(memory_cache: InMemoryCacheProvider) -> None:
|
|
16
|
+
"""Test setting and getting values from memory cache."""
|
|
17
|
+
# Test basic set/get
|
|
18
|
+
await memory_cache.set("test_key", "test_value")
|
|
19
|
+
assert await memory_cache.get("test_key") == "test_value"
|
|
20
|
+
|
|
21
|
+
# Test with different types
|
|
22
|
+
test_data = {
|
|
23
|
+
"string": "hello",
|
|
24
|
+
"int": 42,
|
|
25
|
+
"float": 3.14,
|
|
26
|
+
"list": [1, 2, 3],
|
|
27
|
+
"dict": {"a": 1, "b": 2},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for key, value in test_data.items():
|
|
31
|
+
await memory_cache.set(key, value)
|
|
32
|
+
assert await memory_cache.get(key) == value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_memory_cache_clear(memory_cache: InMemoryCacheProvider) -> None:
|
|
37
|
+
"""Test clearing all values from memory cache."""
|
|
38
|
+
# Add multiple values
|
|
39
|
+
for i in range(5):
|
|
40
|
+
await memory_cache.set(f"key_{i}", f"value_{i}")
|
|
41
|
+
|
|
42
|
+
# Verify values exist
|
|
43
|
+
for i in range(5):
|
|
44
|
+
assert await memory_cache.get(f"key_{i}") == f"value_{i}"
|
|
45
|
+
|
|
46
|
+
# Clear cache
|
|
47
|
+
await memory_cache.clear()
|
|
48
|
+
|
|
49
|
+
# Verify all values are gone
|
|
50
|
+
for i in range(5):
|
|
51
|
+
assert await memory_cache.get(f"key_{i}") is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_memory_cache_nonexistent_key(
|
|
56
|
+
memory_cache: InMemoryCacheProvider,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Test getting a nonexistent key from memory cache."""
|
|
59
|
+
assert await memory_cache.get("nonexistent_key") is None
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
from httpx import Response
|
|
7
7
|
|
|
8
8
|
from port_ocean.clients.port.client import PortClient
|
|
9
|
+
from port_ocean.config.settings import IntegrationSettings, MetricsSettings
|
|
9
10
|
from port_ocean.context.event import EventContext
|
|
10
11
|
from port_ocean.context.ocean import PortOceanContext, ocean
|
|
11
12
|
from port_ocean.core.handlers.entities_state_applier.port.applier import (
|
|
@@ -22,8 +23,10 @@ from port_ocean.core.handlers.port_app_config.models import (
|
|
|
22
23
|
ResourceConfig,
|
|
23
24
|
Selector,
|
|
24
25
|
)
|
|
25
|
-
from port_ocean.core.models import Entity
|
|
26
|
+
from port_ocean.core.models import Entity, ProcessExecutionMode
|
|
27
|
+
from port_ocean.helpers.metric.metric import Metrics
|
|
26
28
|
from port_ocean.ocean import Ocean
|
|
29
|
+
from port_ocean.cache.memory import InMemoryCacheProvider
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
@pytest.fixture
|
|
@@ -83,8 +86,15 @@ def mock_ocean(mock_port_client: PortClient) -> Ocean:
|
|
|
83
86
|
ocean_mock.config.port = MagicMock()
|
|
84
87
|
ocean_mock.config.port.port_app_config_cache_ttl = 60
|
|
85
88
|
ocean_mock.port_client = mock_port_client
|
|
86
|
-
ocean_mock.
|
|
87
|
-
ocean_mock.
|
|
89
|
+
ocean_mock.process_execution_mode = ProcessExecutionMode.single_process
|
|
90
|
+
ocean_mock.cache_provider = InMemoryCacheProvider()
|
|
91
|
+
metrics_settings = MetricsSettings(enabled=True)
|
|
92
|
+
integration_settings = IntegrationSettings(type="test", identifier="test")
|
|
93
|
+
ocean_mock.metrics = Metrics(
|
|
94
|
+
metrics_settings=metrics_settings,
|
|
95
|
+
integration_configuration=integration_settings,
|
|
96
|
+
port_client=mock_port_client,
|
|
97
|
+
)
|
|
88
98
|
|
|
89
99
|
return ocean_mock
|
|
90
100
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from graphlib import CycleError
|
|
2
2
|
from typing import Any, AsyncGenerator
|
|
3
3
|
|
|
4
|
-
from port_ocean.clients.port.client import PortClient
|
|
5
4
|
from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
|
|
6
5
|
from port_ocean.exceptions.core import OceanAbortException
|
|
7
6
|
import pytest
|
|
@@ -25,8 +24,6 @@ from port_ocean.clients.port.types import UserAgentType
|
|
|
25
24
|
from dataclasses import dataclass
|
|
26
25
|
from typing import List, Optional
|
|
27
26
|
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
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
@pytest.fixture
|
|
@@ -94,29 +91,6 @@ def mock_sync_raw_mixin_with_jq_processor(
|
|
|
94
91
|
return mock_sync_raw_mixin
|
|
95
92
|
|
|
96
93
|
|
|
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
|
-
|
|
120
94
|
@pytest.mark.asyncio
|
|
121
95
|
async def test_sync_raw_mixin_self_dependency(
|
|
122
96
|
mock_sync_raw_mixin: SyncRawMixin,
|
|
@@ -213,7 +187,7 @@ async def test_sync_raw_mixin_self_dependency(
|
|
|
213
187
|
metric["metrics"]["phase"]["load"]["object_count_type"][
|
|
214
188
|
"loaded"
|
|
215
189
|
]["object_count"]
|
|
216
|
-
==
|
|
190
|
+
== 2
|
|
217
191
|
)
|
|
218
192
|
|
|
219
193
|
# Verify success
|
|
@@ -341,7 +315,7 @@ async def test_sync_raw_mixin_circular_dependency(
|
|
|
341
315
|
metric["metrics"]["phase"]["load"]["object_count_type"][
|
|
342
316
|
"loaded"
|
|
343
317
|
]["object_count"]
|
|
344
|
-
==
|
|
318
|
+
== 2
|
|
345
319
|
)
|
|
346
320
|
|
|
347
321
|
# Verify success
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
import asyncio
|
|
3
|
-
from port_ocean.utils import cache
|
|
3
|
+
from port_ocean.utils import cache
|
|
4
4
|
import pytest
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
5
|
from typing import AsyncGenerator, AsyncIterator, List, TypeVar
|
|
6
|
+
from unittest.mock import AsyncMock
|
|
7
|
+
from port_ocean.cache.errors import FailedToReadCacheError, FailedToWriteCacheError
|
|
8
|
+
from port_ocean.cache.memory import InMemoryCacheProvider
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
@
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def memory_cache() -> InMemoryCacheProvider:
|
|
13
|
+
return InMemoryCacheProvider()
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@pytest.fixture
|
|
15
|
-
def
|
|
16
|
-
return
|
|
17
|
+
def mock_ocean(memory_cache: InMemoryCacheProvider) -> Any:
|
|
18
|
+
return type(
|
|
19
|
+
"MockOcean",
|
|
20
|
+
(),
|
|
21
|
+
{"app": type("MockApp", (), {"cache_provider": memory_cache})()},
|
|
22
|
+
)()
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
T = TypeVar("T")
|
|
@@ -27,8 +33,8 @@ async def collect_iterator_results(iterator: AsyncIterator[List[T]]) -> List[T]:
|
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
@pytest.mark.asyncio
|
|
30
|
-
async def test_cache_iterator_result(
|
|
31
|
-
monkeypatch.setattr(cache, "
|
|
36
|
+
async def test_cache_iterator_result(mock_ocean: Any, monkeypatch: Any) -> None:
|
|
37
|
+
monkeypatch.setattr(cache, "ocean", mock_ocean)
|
|
32
38
|
|
|
33
39
|
call_count = 0
|
|
34
40
|
|
|
@@ -55,9 +61,9 @@ async def test_cache_iterator_result(event: EventContext, monkeypatch: Any) -> N
|
|
|
55
61
|
|
|
56
62
|
@pytest.mark.asyncio
|
|
57
63
|
async def test_cache_iterator_result_with_kwargs(
|
|
58
|
-
|
|
64
|
+
mock_ocean: Any, monkeypatch: Any
|
|
59
65
|
) -> None:
|
|
60
|
-
monkeypatch.setattr(cache, "
|
|
66
|
+
monkeypatch.setattr(cache, "ocean", mock_ocean)
|
|
61
67
|
|
|
62
68
|
call_count = 0
|
|
63
69
|
|
|
@@ -83,10 +89,16 @@ async def test_cache_iterator_result_with_kwargs(
|
|
|
83
89
|
|
|
84
90
|
|
|
85
91
|
@pytest.mark.asyncio
|
|
86
|
-
async def
|
|
87
|
-
|
|
92
|
+
async def test_cache_iterator_result_cache_errors(
|
|
93
|
+
mock_ocean: Any, monkeypatch: Any
|
|
88
94
|
) -> None:
|
|
89
|
-
|
|
95
|
+
# Create a mock cache provider that raises errors
|
|
96
|
+
mock_cache_provider = AsyncMock()
|
|
97
|
+
mock_cache_provider.get.side_effect = FailedToReadCacheError("fail read")
|
|
98
|
+
mock_cache_provider.set.side_effect = FailedToWriteCacheError("fail write")
|
|
99
|
+
|
|
100
|
+
mock_ocean.app.cache_provider = mock_cache_provider
|
|
101
|
+
monkeypatch.setattr(cache, "ocean", mock_ocean)
|
|
90
102
|
|
|
91
103
|
call_count = 0
|
|
92
104
|
|
|
@@ -98,20 +110,20 @@ async def test_cache_iterator_result_no_cache(
|
|
|
98
110
|
await asyncio.sleep(0.1)
|
|
99
111
|
yield [i]
|
|
100
112
|
|
|
113
|
+
# First call should execute the function since cache read fails
|
|
101
114
|
result1 = await collect_iterator_results(sample_iterator(3))
|
|
102
115
|
assert result1 == [0, 1, 2]
|
|
103
116
|
assert call_count == 1
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
# Second call should also execute the function since cache read fails
|
|
107
119
|
result2 = await collect_iterator_results(sample_iterator(3))
|
|
108
120
|
assert result2 == [0, 1, 2]
|
|
109
121
|
assert call_count == 2
|
|
110
122
|
|
|
111
123
|
|
|
112
124
|
@pytest.mark.asyncio
|
|
113
|
-
async def test_cache_coroutine_result(
|
|
114
|
-
monkeypatch.setattr(cache, "
|
|
125
|
+
async def test_cache_coroutine_result(mock_ocean: Any, monkeypatch: Any) -> None:
|
|
126
|
+
monkeypatch.setattr(cache, "ocean", mock_ocean)
|
|
115
127
|
|
|
116
128
|
call_count = 0
|
|
117
129
|
|
|
@@ -137,9 +149,9 @@ async def test_cache_coroutine_result(event: EventContext, monkeypatch: Any) ->
|
|
|
137
149
|
|
|
138
150
|
@pytest.mark.asyncio
|
|
139
151
|
async def test_cache_coroutine_result_with_kwargs(
|
|
140
|
-
|
|
152
|
+
mock_ocean: Any, monkeypatch: Any
|
|
141
153
|
) -> None:
|
|
142
|
-
monkeypatch.setattr(cache, "
|
|
154
|
+
monkeypatch.setattr(cache, "ocean", mock_ocean)
|
|
143
155
|
|
|
144
156
|
call_count = 0
|
|
145
157
|
|
|
@@ -164,10 +176,16 @@ async def test_cache_coroutine_result_with_kwargs(
|
|
|
164
176
|
|
|
165
177
|
|
|
166
178
|
@pytest.mark.asyncio
|
|
167
|
-
async def
|
|
168
|
-
|
|
179
|
+
async def test_cache_coroutine_result_cache_errors(
|
|
180
|
+
mock_ocean: Any, monkeypatch: Any
|
|
169
181
|
) -> None:
|
|
170
|
-
|
|
182
|
+
# Create a mock cache provider that raises errors
|
|
183
|
+
mock_cache_provider = AsyncMock()
|
|
184
|
+
mock_cache_provider.get.side_effect = FailedToReadCacheError("fail read")
|
|
185
|
+
mock_cache_provider.set.side_effect = FailedToWriteCacheError("fail write")
|
|
186
|
+
|
|
187
|
+
mock_ocean.app.cache_provider = mock_cache_provider
|
|
188
|
+
monkeypatch.setattr(cache, "ocean", mock_ocean)
|
|
171
189
|
|
|
172
190
|
call_count = 0
|
|
173
191
|
|
|
@@ -178,12 +196,79 @@ async def test_cache_coroutine_result_no_cache(
|
|
|
178
196
|
await asyncio.sleep(0.1)
|
|
179
197
|
return x * 2
|
|
180
198
|
|
|
199
|
+
# First call should execute the function since cache read fails
|
|
181
200
|
result1 = await sample_coroutine(2)
|
|
182
201
|
assert result1 == 4
|
|
183
202
|
assert call_count == 1
|
|
184
203
|
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
# Second call should also execute the function since cache read fails
|
|
187
205
|
result2 = await sample_coroutine(2)
|
|
188
206
|
assert result2 == 4
|
|
189
207
|
assert call_count == 2
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_cache_failures_dont_affect_execution(
|
|
212
|
+
mock_ocean: Any, monkeypatch: Any
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Test that cache failures (both read and write) don't affect the decorated function execution."""
|
|
215
|
+
# Create a mock cache provider that raises errors
|
|
216
|
+
mock_cache_provider = AsyncMock()
|
|
217
|
+
mock_cache_provider.get.side_effect = FailedToReadCacheError("fail read")
|
|
218
|
+
mock_cache_provider.set.side_effect = FailedToWriteCacheError("fail write")
|
|
219
|
+
|
|
220
|
+
mock_ocean.app.cache_provider = mock_cache_provider
|
|
221
|
+
monkeypatch.setattr(cache, "ocean", mock_ocean)
|
|
222
|
+
|
|
223
|
+
# Test both iterator and coroutine decorators
|
|
224
|
+
iterator_call_count = 0
|
|
225
|
+
coroutine_call_count = 0
|
|
226
|
+
|
|
227
|
+
@cache.cache_iterator_result()
|
|
228
|
+
async def sample_iterator(x: int) -> AsyncGenerator[List[int], None]:
|
|
229
|
+
nonlocal iterator_call_count
|
|
230
|
+
iterator_call_count += 1
|
|
231
|
+
for i in range(x):
|
|
232
|
+
await asyncio.sleep(0.1)
|
|
233
|
+
yield [i]
|
|
234
|
+
|
|
235
|
+
@cache.cache_coroutine_result()
|
|
236
|
+
async def sample_coroutine(x: int) -> int:
|
|
237
|
+
nonlocal coroutine_call_count
|
|
238
|
+
coroutine_call_count += 1
|
|
239
|
+
await asyncio.sleep(0.1)
|
|
240
|
+
return x * 2
|
|
241
|
+
|
|
242
|
+
# Test iterator function
|
|
243
|
+
# First call - should execute function (cache read fails)
|
|
244
|
+
result1 = await collect_iterator_results(sample_iterator(3))
|
|
245
|
+
assert result1 == [0, 1, 2]
|
|
246
|
+
assert iterator_call_count == 1
|
|
247
|
+
assert mock_cache_provider.get.call_count == 1
|
|
248
|
+
assert mock_cache_provider.set.call_count == 1
|
|
249
|
+
|
|
250
|
+
# Second call - should execute function again (cache read fails)
|
|
251
|
+
result2 = await collect_iterator_results(sample_iterator(3))
|
|
252
|
+
assert result2 == [0, 1, 2]
|
|
253
|
+
assert iterator_call_count == 2
|
|
254
|
+
assert mock_cache_provider.get.call_count == 2
|
|
255
|
+
assert mock_cache_provider.set.call_count == 2
|
|
256
|
+
|
|
257
|
+
# Test coroutine function
|
|
258
|
+
# First call - should execute function (cache read fails)
|
|
259
|
+
result3 = await sample_coroutine(4)
|
|
260
|
+
assert result3 == 8
|
|
261
|
+
assert coroutine_call_count == 1
|
|
262
|
+
assert mock_cache_provider.get.call_count == 3
|
|
263
|
+
assert mock_cache_provider.set.call_count == 3
|
|
264
|
+
|
|
265
|
+
# Second call - should execute function again (cache read fails)
|
|
266
|
+
result4 = await sample_coroutine(4)
|
|
267
|
+
assert result4 == 8
|
|
268
|
+
assert coroutine_call_count == 2
|
|
269
|
+
assert mock_cache_provider.get.call_count == 4
|
|
270
|
+
assert mock_cache_provider.set.call_count == 4
|
|
271
|
+
|
|
272
|
+
# Verify that both read and write errors were raised
|
|
273
|
+
assert isinstance(mock_cache_provider.get.side_effect, FailedToReadCacheError)
|
|
274
|
+
assert isinstance(mock_cache_provider.set.side_effect, FailedToWriteCacheError)
|
port_ocean/utils/cache.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import hashlib
|
|
3
|
+
import base64
|
|
3
4
|
from typing import Callable, AsyncIterator, Awaitable, Any
|
|
4
|
-
from port_ocean.
|
|
5
|
+
from port_ocean.cache.errors import FailedToReadCacheError, FailedToWriteCacheError
|
|
6
|
+
from port_ocean.context.ocean import ocean
|
|
7
|
+
from loguru import logger
|
|
5
8
|
|
|
6
9
|
AsyncIteratorCallable = Callable[..., AsyncIterator[list[Any]]]
|
|
7
10
|
AsyncCallable = Callable[..., Awaitable[Any]]
|
|
@@ -12,15 +15,18 @@ def hash_func(function_name: str, *args: Any, **kwargs: Any) -> str:
|
|
|
12
15
|
kwargs_str = str(kwargs)
|
|
13
16
|
concatenated_string = args_str + kwargs_str
|
|
14
17
|
hash_object = hashlib.sha256(concatenated_string.encode())
|
|
15
|
-
|
|
18
|
+
short_hash = base64.urlsafe_b64encode(hash_object.digest()[:8]).decode("ascii")
|
|
19
|
+
short_hash = short_hash.rstrip("=").replace("-", "_").replace("+", "_")
|
|
20
|
+
return f"{function_name}_{short_hash}"
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
def cache_iterator_result() -> Callable[[AsyncIteratorCallable], AsyncIteratorCallable]:
|
|
19
24
|
"""
|
|
20
25
|
This decorator caches the results of an async iterator function. It checks if the result is already in the cache
|
|
21
|
-
and if not, it fetches the all the data and caches it at
|
|
26
|
+
and if not, it fetches the all the data and caches it at the end of the iteration.
|
|
22
27
|
|
|
23
28
|
The cache will be stored in the scope of the running event and will be removed when the event is finished.
|
|
29
|
+
If a database is configured, the cache will also be stored in the database.
|
|
24
30
|
|
|
25
31
|
For example, you can use this to cache data coming back from the third-party API to avoid making the same request
|
|
26
32
|
multiple times for each kind.
|
|
@@ -39,13 +45,15 @@ def cache_iterator_result() -> Callable[[AsyncIteratorCallable], AsyncIteratorCa
|
|
|
39
45
|
def decorator(func: AsyncIteratorCallable) -> AsyncIteratorCallable:
|
|
40
46
|
@functools.wraps(func)
|
|
41
47
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
42
|
-
# Create Hash key from function name, args and kwargs
|
|
43
48
|
cache_key = hash_func(func.__name__, *args, **kwargs)
|
|
44
49
|
|
|
45
50
|
# Check if the result is already in the cache
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
try:
|
|
52
|
+
if cache := await ocean.app.cache_provider.get(cache_key):
|
|
53
|
+
yield cache
|
|
54
|
+
return
|
|
55
|
+
except FailedToReadCacheError as e:
|
|
56
|
+
logger.warning(f"Failed to read cache for {cache_key}: {str(e)}")
|
|
49
57
|
|
|
50
58
|
# If not in cache, fetch the data
|
|
51
59
|
cached_results = list()
|
|
@@ -54,7 +62,13 @@ def cache_iterator_result() -> Callable[[AsyncIteratorCallable], AsyncIteratorCa
|
|
|
54
62
|
yield result
|
|
55
63
|
|
|
56
64
|
# Cache the results
|
|
57
|
-
|
|
65
|
+
try:
|
|
66
|
+
await ocean.app.cache_provider.set(
|
|
67
|
+
cache_key,
|
|
68
|
+
cached_results,
|
|
69
|
+
)
|
|
70
|
+
except FailedToWriteCacheError as e:
|
|
71
|
+
logger.warning(f"Failed to write cache for {cache_key}: {str(e)}")
|
|
58
72
|
return
|
|
59
73
|
|
|
60
74
|
return wrapper
|
|
@@ -71,6 +85,7 @@ def cache_coroutine_result() -> Callable[[AsyncCallable], AsyncCallable]:
|
|
|
71
85
|
|
|
72
86
|
The cache is stored in the scope of the running event and is
|
|
73
87
|
removed when the event is finished.
|
|
88
|
+
If a database is configured, the cache will also be stored in the database.
|
|
74
89
|
|
|
75
90
|
Usage:
|
|
76
91
|
```python
|
|
@@ -84,12 +99,20 @@ def cache_coroutine_result() -> Callable[[AsyncCallable], AsyncCallable]:
|
|
|
84
99
|
@functools.wraps(func)
|
|
85
100
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
86
101
|
cache_key = hash_func(func.__name__, *args, **kwargs)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
try:
|
|
103
|
+
if cache := await ocean.app.cache_provider.get(cache_key):
|
|
104
|
+
return cache
|
|
105
|
+
except FailedToReadCacheError as e:
|
|
106
|
+
logger.warning(f"Failed to read cache for {cache_key}: {str(e)}")
|
|
90
107
|
|
|
91
108
|
result = await func(*args, **kwargs)
|
|
92
|
-
|
|
109
|
+
try:
|
|
110
|
+
await ocean.app.cache_provider.set(
|
|
111
|
+
cache_key,
|
|
112
|
+
result,
|
|
113
|
+
)
|
|
114
|
+
except FailedToWriteCacheError as e:
|
|
115
|
+
logger.warning(f"Failed to write cache for {cache_key}: {str(e)}")
|
|
93
116
|
return result
|
|
94
117
|
|
|
95
118
|
return wrapper
|
port_ocean/utils/ipc.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FileIPC:
|
|
7
|
+
def __init__(self, process_id: str, name: str, default_return: Any = None):
|
|
8
|
+
self.process_id = process_id
|
|
9
|
+
self.name = name
|
|
10
|
+
self.dir_path = f"/tmp/p_{self.process_id}"
|
|
11
|
+
self.file_path = f"{self.dir_path}/{self.name}.pkl"
|
|
12
|
+
self.default_return = default_return
|
|
13
|
+
os.makedirs(self.dir_path, exist_ok=True)
|
|
14
|
+
|
|
15
|
+
def __del__(self) -> None:
|
|
16
|
+
self.delete()
|
|
17
|
+
|
|
18
|
+
def save(self, object: Any) -> None:
|
|
19
|
+
with open(self.file_path, "wb") as f:
|
|
20
|
+
pickle.dump(object, f)
|
|
21
|
+
|
|
22
|
+
def load(self) -> Any:
|
|
23
|
+
if not os.path.exists(self.file_path):
|
|
24
|
+
return self.default_return
|
|
25
|
+
with open(self.file_path, "rb") as f:
|
|
26
|
+
return pickle.load(f)
|
|
27
|
+
|
|
28
|
+
def delete(self) -> None:
|
|
29
|
+
if os.path.exists(self.file_path):
|
|
30
|
+
os.remove(self.file_path)
|
|
@@ -3,11 +3,18 @@ integrations/_infra/Dockerfile.alpine,sha256=7E4Sb-8supsCcseerHwTkuzjHZoYcaHIyxi
|
|
|
3
3
|
integrations/_infra/Dockerfile.base.builder,sha256=Ogp_fodUE-lD-HgcfcFJd7pr520LPoEU9GCNk6HQBxk,619
|
|
4
4
|
integrations/_infra/Dockerfile.base.runner,sha256=T7Tis9EjmcOl2jUXPMSENbRXMqwoHnnFPhgRmfEMO5Y,357
|
|
5
5
|
integrations/_infra/Dockerfile.dockerignore,sha256=CM1Fxt3I2AvSvObuUZRmy5BNLSGC7ylnbpWzFgD4cso,1163
|
|
6
|
+
integrations/_infra/Dockerfile.local,sha256=Aqj3y4U6XFS78i5Zz3IfyZkvVmAdB7eEAe6khQaxRxI,876
|
|
6
7
|
integrations/_infra/Makefile,sha256=YgLKvuF_Dw4IA7X98Nus6zIW_3cJ60M1QFGs3imj5c4,2430
|
|
8
|
+
integrations/_infra/entry_local.sh,sha256=cH2Gd82qDnLKXvjoK1MNay9vdIZzTTF_hrhmvZuYZbg,648
|
|
7
9
|
integrations/_infra/grpcio.sh,sha256=m924poYznoRZ6Tt7Ct8Cs5AV_cmmOx598yIZ3z4DvZE,616
|
|
8
10
|
integrations/_infra/init.sh,sha256=nN8lTrOhB286UfFvD6sJ9YJ-9asT9zVSddQB-RAb7Z4,99
|
|
9
|
-
port_ocean/__init__.py,sha256=
|
|
11
|
+
port_ocean/__init__.py,sha256=uMpjg5d_cXgnyCxA_LmICR8zqBmC6Fe9Ivu9hcvJ7EY,313
|
|
10
12
|
port_ocean/bootstrap.py,sha256=CN1M5pVecZ7z_Vfu86Dk2HjFMiuiwt6E_SSOLFCYRMk,1321
|
|
13
|
+
port_ocean/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
port_ocean/cache/base.py,sha256=XfsIfbE5y22pqr18sJMCCT4kS6h6BztHNpiH2aoubGM,661
|
|
15
|
+
port_ocean/cache/disk.py,sha256=f1FHP74qWwVxMs4UdOzyoTWW0mPVTwfka8rITLi3ISg,1903
|
|
16
|
+
port_ocean/cache/errors.py,sha256=KZ7c3L9k1e0Btw-BBERAxjONKkmQoH7EllTvFEiqQEA,145
|
|
17
|
+
port_ocean/cache/memory.py,sha256=w4Jhvpa56j1vQ1XQ-XFe8KQPVkVzLsF8RmgwZm4XUCA,1246
|
|
11
18
|
port_ocean/cli/__init__.py,sha256=ZjTGS305llhbjC2BH2KkVj34gCASBGwqc5HZEO_0T_Q,328
|
|
12
19
|
port_ocean/cli/cli.py,sha256=RvWTELEn5YFw9aM0vaNqm5YqZZrL50ILaBs27ptiGl0,57
|
|
13
20
|
port_ocean/cli/commands/__init__.py,sha256=Y9Q6jeYw_ZAZ-mdfE_5DZTdS2KHhieQZoUTggk_AkwM,369
|
|
@@ -62,7 +69,7 @@ port_ocean/clients/port/utils.py,sha256=osFyAjw7Y5Qf2uVSqC7_RTCQfijiL1zS74JJM0go
|
|
|
62
69
|
port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
70
|
port_ocean/config/base.py,sha256=x1gFbzujrxn7EJudRT81C6eN9WsYAb3vOHwcpcpX8Tc,6370
|
|
64
71
|
port_ocean/config/dynamic.py,sha256=qOFkRoJsn_BW7581omi_AoMxoHqasf_foxDQ_G11_SI,2030
|
|
65
|
-
port_ocean/config/settings.py,sha256=
|
|
72
|
+
port_ocean/config/settings.py,sha256=kVXF5_Jr93qW4xDlYXbfehDlQjpv4REjiSAQWePKfYs,6438
|
|
66
73
|
port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
74
|
port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
|
|
68
75
|
port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -113,9 +120,9 @@ port_ocean/core/integrations/mixins/events.py,sha256=2L7P3Jhp8XBqddh2_o9Cn4N261n
|
|
|
113
120
|
port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
|
|
114
121
|
port_ocean/core/integrations/mixins/live_events.py,sha256=8HklZmlyffYY_LeDe8xbt3Tb08rlLkqVhFF-2NQeJP4,4126
|
|
115
122
|
port_ocean/core/integrations/mixins/sync.py,sha256=Vm_898pLKBwfVewtwouDWsXoxcOLicnAy6pzyqqk6U8,4053
|
|
116
|
-
port_ocean/core/integrations/mixins/sync_raw.py,sha256=
|
|
117
|
-
port_ocean/core/integrations/mixins/utils.py,sha256=
|
|
118
|
-
port_ocean/core/models.py,sha256=
|
|
123
|
+
port_ocean/core/integrations/mixins/sync_raw.py,sha256=TCxWRyHu5p7Pn1qnl-crqFrf_CoOLEGo7wGs1XRS3ys,32284
|
|
124
|
+
port_ocean/core/integrations/mixins/utils.py,sha256=_Pax41H3U75HZRPkrFbcSYdT4NDGb1LtdF_anuIcj1o,2347
|
|
125
|
+
port_ocean/core/models.py,sha256=MKfq69zGbFRzo0I2HRDUvSbz_pjrtcFVsD5B4Qwa3fw,2538
|
|
119
126
|
port_ocean/core/ocean_types.py,sha256=4VipWFOHEh_d9LmWewQccwx1p2dtrRYW0YURVgNsAjo,1398
|
|
120
127
|
port_ocean/core/utils/entity_topological_sorter.py,sha256=MDUjM6OuDy4Xj68o-7InNN0w1jqjxeDfeY8U02vySNI,3081
|
|
121
128
|
port_ocean/core/utils/utils.py,sha256=XJ6ZZBR5hols19TcX4Bh49ygSNhPt3MLncLR-g41GTA,6858
|
|
@@ -131,7 +138,7 @@ port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnq
|
|
|
131
138
|
port_ocean/exceptions/webhook_processor.py,sha256=yQYazg53Y-ohb7HfViwq1opH_ZUuUdhHSRxcUNveFpI,114
|
|
132
139
|
port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
133
140
|
port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861vE3K40,1783
|
|
134
|
-
port_ocean/helpers/metric/metric.py,sha256=
|
|
141
|
+
port_ocean/helpers/metric/metric.py,sha256=iktHKXQNzkLYHgCLWA5wxRDvAMJrJIMoGQYFXV83mH0,12973
|
|
135
142
|
port_ocean/helpers/metric/utils.py,sha256=Wnr-6HwVwBtYJ3so44OkhDRs8udLMSB1oduzl2-zRHo,781
|
|
136
143
|
port_ocean/helpers/retry.py,sha256=gmS4YxM6N4fboFp7GSgtOzyBJemxs46bnrz4L4rDS6Y,16136
|
|
137
144
|
port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -139,23 +146,26 @@ port_ocean/log/handlers.py,sha256=ncVjgqrZRh6BhyRrA6DQG86Wsbxph1yWYuEC0cWfe-Q,36
|
|
|
139
146
|
port_ocean/log/logger_setup.py,sha256=0K3zVG0YYrYOWEV8-rCGks1o-bMRxgHXlqawu9w_tSw,2656
|
|
140
147
|
port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2920
|
|
141
148
|
port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
|
|
142
|
-
port_ocean/ocean.py,sha256=
|
|
149
|
+
port_ocean/ocean.py,sha256=h0d-lOf7FQdrRylEglD1MqNzHk-OdvwAorFWyiT6UBo,8825
|
|
143
150
|
port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
144
|
-
port_ocean/run.py,sha256=
|
|
151
|
+
port_ocean/run.py,sha256=CmKz14bxfdOooNbQ5QqH1MwX-XLYVG4NgT4KbrzFaqI,2216
|
|
145
152
|
port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
|
|
146
153
|
port_ocean/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
154
|
+
port_ocean/tests/cache/__init__.py,sha256=mFSl2_of1u8WxZIypvvHkobHsukM3_RjUfCMZQtzZeI,33
|
|
155
|
+
port_ocean/tests/cache/test_disk_cache.py,sha256=5-GXpUepomW4yCWkm43LwzNKlMmH6BOa5cYaXccXAKQ,2702
|
|
156
|
+
port_ocean/tests/cache/test_memory_cache.py,sha256=xlwIOBU0RVLYYJU83l_aoZDzZ6QIDwz-fcKfRkR6dFk,1653
|
|
147
157
|
port_ocean/tests/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
148
158
|
port_ocean/tests/clients/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
149
159
|
port_ocean/tests/clients/oauth/test_oauth_client.py,sha256=2XVMQUalDpiD539Z7_dk5BK_ngXQzsTmb2lNBsfEm9c,3266
|
|
150
160
|
port_ocean/tests/clients/port/mixins/test_entities.py,sha256=Zq_wKTymxJ0R8lHKztvEV6lN__3FZk8uTSIVpKCE6NA,1815
|
|
151
161
|
port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=zzKYz3h8dl4Z5A2QG_924m0y9U6XTth1XYOfwNrd_24,914
|
|
152
162
|
port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
|
|
153
|
-
port_ocean/tests/core/conftest.py,sha256=
|
|
163
|
+
port_ocean/tests/core/conftest.py,sha256=tTOxB8HlCmXgSsXhtPI6xYgdpbpemxfBhA_gBDBuACQ,6115
|
|
154
164
|
port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
|
|
155
165
|
port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=WNg1fWZsXu0MDnz9-ahRiPb_OPofWx7E8wxBx0cyZKs,8946
|
|
156
166
|
port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=8WpMn559Mf0TFWmloRpZrVgr6yWwyA0C4n2lVHCtyq4,13596
|
|
157
167
|
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=
|
|
168
|
+
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=QHFigCpvYHf6sVirLBI23pMFyZCn4TcWaaFbHtUjoFA,42441
|
|
159
169
|
port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
|
|
160
170
|
port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=hSh556bJM9zuELwhwnyKSfd9z06WqWXIfe-6hCl5iKI,9799
|
|
161
171
|
port_ocean/tests/core/handlers/queue/test_local_queue.py,sha256=9Ly0HzZXbs6Rbl_bstsIdInC3h2bgABU3roP9S_PnJM,2582
|
|
@@ -177,19 +187,20 @@ port_ocean/tests/test_metric.py,sha256=gDdeJcqJDQ_o3VrYrW23iZyw2NuUsyATdrygSXhcD
|
|
|
177
187
|
port_ocean/tests/test_ocean.py,sha256=bsXKGTVEjwLSbR7-qSmI4GZ-EzDo0eBE3TNSMsWzYxM,1502
|
|
178
188
|
port_ocean/tests/test_smoke.py,sha256=uix2uIg_yOm8BHDgHw2hTFPy1fiIyxBGW3ENU_KoFlo,2557
|
|
179
189
|
port_ocean/tests/utils/test_async_iterators.py,sha256=3PLk1emEXekb8LcC5GgVh3OicaX15i5WyaJT_eFnu_4,1336
|
|
180
|
-
port_ocean/tests/utils/test_cache.py,sha256=
|
|
190
|
+
port_ocean/tests/utils/test_cache.py,sha256=MIYzHt1DeVJ_2KNpVfnUaivNlmdzXDBC5ZeixJPPKL8,8591
|
|
181
191
|
port_ocean/utils/__init__.py,sha256=KMGnCPXZJbNwtgxtyMycapkDz8tpSyw23MSYT3iVeHs,91
|
|
182
192
|
port_ocean/utils/async_http.py,sha256=aDsw3gQIMwt6qLegbZtkHqD8em48tKvbITnblsrTY3g,1260
|
|
183
193
|
port_ocean/utils/async_iterators.py,sha256=CPXskYWkhkZtAG-ducEwM8537t3z5usPEqXR9vcivzw,3715
|
|
184
|
-
port_ocean/utils/cache.py,sha256=
|
|
194
|
+
port_ocean/utils/cache.py,sha256=tRwPomG2VIxx8ZNi4QYH6Yc47d9yYV1A7Hx-L_fX4Dg,4494
|
|
195
|
+
port_ocean/utils/ipc.py,sha256=BMVUxdftf0i7Z2Xp8KMFlttUjZhTE7VUCpY4SBBnoVY,896
|
|
185
196
|
port_ocean/utils/misc.py,sha256=0q2cJ5psqxn_5u_56pT7vOVQ3shDM02iC1lzyWQ_zl0,2098
|
|
186
197
|
port_ocean/utils/queue_utils.py,sha256=KWWl8YVnG-glcfIHhM6nefY-2sou_C6DVP1VynQwzB4,2762
|
|
187
198
|
port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,3229
|
|
188
199
|
port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
|
|
189
200
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
|
190
201
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
|
191
|
-
port_ocean-0.
|
|
192
|
-
port_ocean-0.
|
|
193
|
-
port_ocean-0.
|
|
194
|
-
port_ocean-0.
|
|
195
|
-
port_ocean-0.
|
|
202
|
+
port_ocean-0.23.0.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
203
|
+
port_ocean-0.23.0.dist-info/METADATA,sha256=RAOhyPDxeKshLjPZ4hTM8uKjumyU2GZgpJDcjjNC-jY,6764
|
|
204
|
+
port_ocean-0.23.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
205
|
+
port_ocean-0.23.0.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
|
206
|
+
port_ocean-0.23.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|