port-ocean 0.5.5__py3-none-any.whl → 0.17.8__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 +56 -0
- integrations/_infra/Dockerfile.alpine +108 -0
- integrations/_infra/Dockerfile.base.builder +26 -0
- integrations/_infra/Dockerfile.base.runner +13 -0
- integrations/_infra/Dockerfile.dockerignore +94 -0
- {port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}} → integrations/_infra}/Makefile +21 -8
- integrations/_infra/grpcio.sh +18 -0
- integrations/_infra/init.sh +5 -0
- port_ocean/bootstrap.py +1 -1
- port_ocean/cli/commands/defaults/clean.py +3 -1
- port_ocean/cli/commands/new.py +42 -7
- port_ocean/cli/commands/sail.py +7 -1
- port_ocean/cli/cookiecutter/cookiecutter.json +3 -0
- port_ocean/cli/cookiecutter/hooks/post_gen_project.py +20 -3
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +6 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +41 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +16 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +6 -7
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +1 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +7 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +1 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +16 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +21 -10
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +2 -0
- port_ocean/clients/port/authentication.py +16 -4
- port_ocean/clients/port/client.py +17 -0
- port_ocean/clients/port/mixins/blueprints.py +7 -8
- port_ocean/clients/port/mixins/entities.py +108 -53
- port_ocean/clients/port/mixins/integrations.py +23 -34
- port_ocean/clients/port/retry_transport.py +0 -5
- port_ocean/clients/port/utils.py +9 -3
- port_ocean/config/base.py +16 -16
- port_ocean/config/dynamic.py +2 -0
- port_ocean/config/settings.py +79 -11
- port_ocean/context/event.py +18 -5
- port_ocean/context/ocean.py +14 -3
- port_ocean/core/defaults/clean.py +10 -3
- port_ocean/core/defaults/common.py +25 -9
- port_ocean/core/defaults/initialize.py +111 -100
- port_ocean/core/event_listener/__init__.py +8 -0
- port_ocean/core/event_listener/base.py +49 -10
- port_ocean/core/event_listener/factory.py +9 -1
- port_ocean/core/event_listener/http.py +11 -3
- port_ocean/core/event_listener/kafka.py +24 -5
- port_ocean/core/event_listener/once.py +96 -4
- port_ocean/core/event_listener/polling.py +16 -14
- port_ocean/core/event_listener/webhooks_only.py +41 -0
- port_ocean/core/handlers/__init__.py +1 -2
- port_ocean/core/handlers/entities_state_applier/base.py +4 -1
- port_ocean/core/handlers/entities_state_applier/port/applier.py +29 -87
- port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +5 -2
- port_ocean/core/handlers/entity_processor/base.py +26 -22
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py +253 -45
- port_ocean/core/handlers/port_app_config/base.py +55 -15
- port_ocean/core/handlers/port_app_config/models.py +24 -5
- port_ocean/core/handlers/resync_state_updater/__init__.py +5 -0
- port_ocean/core/handlers/resync_state_updater/updater.py +84 -0
- port_ocean/core/integrations/base.py +5 -7
- port_ocean/core/integrations/mixins/events.py +3 -1
- port_ocean/core/integrations/mixins/sync.py +4 -2
- port_ocean/core/integrations/mixins/sync_raw.py +209 -74
- port_ocean/core/integrations/mixins/utils.py +1 -1
- port_ocean/core/models.py +44 -0
- port_ocean/core/ocean_types.py +29 -11
- port_ocean/core/utils/entity_topological_sorter.py +90 -0
- port_ocean/core/utils/utils.py +109 -0
- port_ocean/debug_cli.py +5 -0
- port_ocean/exceptions/core.py +4 -0
- port_ocean/exceptions/port_defaults.py +0 -2
- port_ocean/helpers/retry.py +85 -24
- port_ocean/log/handlers.py +23 -2
- port_ocean/log/logger_setup.py +8 -1
- port_ocean/log/sensetive.py +25 -10
- port_ocean/middlewares.py +10 -2
- port_ocean/ocean.py +57 -24
- port_ocean/run.py +10 -5
- port_ocean/tests/__init__.py +0 -0
- port_ocean/tests/clients/port/mixins/test_entities.py +53 -0
- port_ocean/tests/conftest.py +4 -0
- port_ocean/tests/core/defaults/test_common.py +166 -0
- port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +350 -0
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +552 -0
- port_ocean/tests/core/test_utils.py +73 -0
- port_ocean/tests/core/utils/test_entity_topological_sorter.py +99 -0
- port_ocean/tests/helpers/__init__.py +0 -0
- port_ocean/tests/helpers/fake_port_api.py +191 -0
- port_ocean/tests/helpers/fixtures.py +46 -0
- port_ocean/tests/helpers/integration.py +31 -0
- port_ocean/tests/helpers/ocean_app.py +66 -0
- port_ocean/tests/helpers/port_client.py +21 -0
- port_ocean/tests/helpers/smoke_test.py +82 -0
- port_ocean/tests/log/test_handlers.py +71 -0
- port_ocean/tests/test_smoke.py +74 -0
- port_ocean/tests/utils/test_async_iterators.py +45 -0
- port_ocean/tests/utils/test_cache.py +189 -0
- port_ocean/utils/async_iterators.py +109 -0
- port_ocean/utils/cache.py +37 -1
- port_ocean/utils/misc.py +22 -4
- port_ocean/utils/queue_utils.py +88 -0
- port_ocean/utils/signal.py +1 -4
- port_ocean/utils/time.py +54 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/METADATA +27 -19
- port_ocean-0.17.8.dist-info/RECORD +164 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/WHEEL +1 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -94
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -15
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -17
- port_ocean/core/handlers/entities_state_applier/port/validate_entity_relations.py +0 -40
- port_ocean/core/utils.py +0 -65
- port_ocean-0.5.5.dist-info/RECORD +0 -129
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/entry_points.txt +0 -0
port_ocean/run.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from inspect import getmembers
|
|
2
3
|
from typing import Dict, Any, Type
|
|
3
4
|
|
|
@@ -8,9 +9,11 @@ from port_ocean.bootstrap import create_default_app
|
|
|
8
9
|
from port_ocean.config.dynamic import default_config_factory
|
|
9
10
|
from port_ocean.config.settings import ApplicationSettings, LogLevelType
|
|
10
11
|
from port_ocean.core.defaults.initialize import initialize_defaults
|
|
12
|
+
from port_ocean.core.utils.utils import validate_integration_runtime
|
|
11
13
|
from port_ocean.log.logger_setup import setup_logger
|
|
12
14
|
from port_ocean.ocean import Ocean
|
|
13
15
|
from port_ocean.utils.misc import get_spec_file, load_module
|
|
16
|
+
from port_ocean.utils.signal import init_signal_handler
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def _get_default_config_factory() -> None | Type[BaseModel]:
|
|
@@ -31,6 +34,7 @@ def run(
|
|
|
31
34
|
) -> None:
|
|
32
35
|
application_settings = ApplicationSettings(log_level=log_level, port=port)
|
|
33
36
|
|
|
37
|
+
init_signal_handler()
|
|
34
38
|
setup_logger(
|
|
35
39
|
application_settings.log_level,
|
|
36
40
|
enable_http_handler=application_settings.enable_http_logging,
|
|
@@ -45,13 +49,14 @@ def run(
|
|
|
45
49
|
"app", default_app
|
|
46
50
|
)
|
|
47
51
|
|
|
52
|
+
# Validate that the current integration's runtime matches the execution parameters
|
|
53
|
+
asyncio.get_event_loop().run_until_complete(
|
|
54
|
+
validate_integration_runtime(app.port_client, app.config.runtime)
|
|
55
|
+
)
|
|
56
|
+
|
|
48
57
|
# Override config with arguments
|
|
49
58
|
if initialize_port_resources is not None:
|
|
50
59
|
app.config.initialize_port_resources = initialize_port_resources
|
|
51
|
-
|
|
52
|
-
if app.config.initialize_port_resources:
|
|
53
|
-
initialize_defaults(
|
|
54
|
-
app.integration.AppConfigHandlerClass.CONFIG_CLASS, app.config
|
|
55
|
-
)
|
|
60
|
+
initialize_defaults(app.integration.AppConfigHandlerClass.CONFIG_CLASS, app.config)
|
|
56
61
|
|
|
57
62
|
uvicorn.run(app, host="0.0.0.0", port=application_settings.port)
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from port_ocean.clients.port.mixins.entities import EntityClientMixin
|
|
7
|
+
from port_ocean.core.models import Entity
|
|
8
|
+
from httpx import ReadTimeout
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
errored_entity_identifier: str = "a"
|
|
12
|
+
expected_result_entities = [
|
|
13
|
+
Entity(identifier="b", blueprint="b"),
|
|
14
|
+
Entity(identifier="c", blueprint="c"),
|
|
15
|
+
]
|
|
16
|
+
all_entities = [
|
|
17
|
+
Entity(identifier=errored_entity_identifier, blueprint="a")
|
|
18
|
+
] + expected_result_entities
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def mock_upsert_entity(entity: Entity, *args: Any, **kwargs: Any) -> Entity:
|
|
22
|
+
if entity.identifier == errored_entity_identifier:
|
|
23
|
+
raise ReadTimeout("")
|
|
24
|
+
else:
|
|
25
|
+
return entity
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
async def entity_client(monkeypatch: Any) -> EntityClientMixin:
|
|
30
|
+
# Arrange
|
|
31
|
+
entity_client = EntityClientMixin(auth=MagicMock(), client=MagicMock())
|
|
32
|
+
monkeypatch.setattr(entity_client, "upsert_entity", mock_upsert_entity)
|
|
33
|
+
|
|
34
|
+
return entity_client
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def test_batch_upsert_entities_read_timeout_should_raise_false(
|
|
38
|
+
entity_client: EntityClientMixin,
|
|
39
|
+
) -> None:
|
|
40
|
+
result_entities = await entity_client.batch_upsert_entities(
|
|
41
|
+
entities=all_entities, request_options=MagicMock(), should_raise=False
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert result_entities == expected_result_entities
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def test_batch_upsert_entities_read_timeout_should_raise_true(
|
|
48
|
+
entity_client: EntityClientMixin,
|
|
49
|
+
) -> None:
|
|
50
|
+
with pytest.raises(ReadTimeout):
|
|
51
|
+
await entity_client.batch_upsert_entities(
|
|
52
|
+
entities=all_entities, request_options=MagicMock(), should_raise=True
|
|
53
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import json
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from port_ocean.core.handlers.port_app_config.models import PortAppConfig
|
|
6
|
+
from port_ocean.core.defaults.common import (
|
|
7
|
+
get_port_integration_defaults,
|
|
8
|
+
Defaults,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def setup_mock_directories(tmp_path: Path) -> tuple[Path, Path, Path]:
|
|
14
|
+
# Create .port/resources with sample files
|
|
15
|
+
default_dir = tmp_path / ".port/resources"
|
|
16
|
+
default_dir.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
# Create mock JSON and YAML files with expected content
|
|
19
|
+
(default_dir / "blueprints.json").write_text(
|
|
20
|
+
json.dumps(
|
|
21
|
+
[
|
|
22
|
+
{
|
|
23
|
+
"identifier": "mock-identifier",
|
|
24
|
+
"title": "mock-title",
|
|
25
|
+
"icon": "mock-icon",
|
|
26
|
+
"schema": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {"key": {"type": "string"}},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
(default_dir / "port-app-config.json").write_text(
|
|
35
|
+
json.dumps(
|
|
36
|
+
{
|
|
37
|
+
"resources": [
|
|
38
|
+
{
|
|
39
|
+
"kind": "mock-kind",
|
|
40
|
+
"selector": {"query": "true"},
|
|
41
|
+
"port": {
|
|
42
|
+
"entity": {
|
|
43
|
+
"mappings": {
|
|
44
|
+
"identifier": ".id",
|
|
45
|
+
"title": ".title",
|
|
46
|
+
"blueprint": '"mock-identifier"',
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Create .port/custom_resources with different sample files
|
|
57
|
+
custom_resources_dir = tmp_path / ".port/custom_resources"
|
|
58
|
+
custom_resources_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# Create mock JSON and YAML files with expected content
|
|
61
|
+
(custom_resources_dir / "blueprints.json").write_text(
|
|
62
|
+
json.dumps(
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
"identifier": "mock-custom-identifier",
|
|
66
|
+
"title": "mock-custom-title",
|
|
67
|
+
"icon": "mock-custom-icon",
|
|
68
|
+
"schema": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {"key": {"type": "string"}},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
(custom_resources_dir / "port-app-config.json").write_text(
|
|
77
|
+
json.dumps(
|
|
78
|
+
{
|
|
79
|
+
"resources": [
|
|
80
|
+
{
|
|
81
|
+
"kind": "mock-custom-kind",
|
|
82
|
+
"selector": {"query": "true"},
|
|
83
|
+
"port": {
|
|
84
|
+
"entity": {
|
|
85
|
+
"mappings": {
|
|
86
|
+
"identifier": ".id",
|
|
87
|
+
"title": ".title",
|
|
88
|
+
"blueprint": '"mock-custom-identifier"',
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Define the non-existing directory path
|
|
99
|
+
non_existing_dir = tmp_path / ".port/do_not_exist"
|
|
100
|
+
|
|
101
|
+
return default_dir, custom_resources_dir, non_existing_dir
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_custom_defaults_dir_used_if_valid(
|
|
105
|
+
setup_mock_directories: tuple[Path, Path, Path]
|
|
106
|
+
) -> None:
|
|
107
|
+
# Arrange
|
|
108
|
+
_, custom_resources_dir, _ = setup_mock_directories
|
|
109
|
+
|
|
110
|
+
with (
|
|
111
|
+
patch("port_ocean.core.defaults.common.is_valid_dir") as mock_is_valid_dir,
|
|
112
|
+
patch(
|
|
113
|
+
"pathlib.Path.iterdir",
|
|
114
|
+
return_value=custom_resources_dir.iterdir(),
|
|
115
|
+
),
|
|
116
|
+
):
|
|
117
|
+
mock_is_valid_dir.side_effect = lambda path: path == custom_resources_dir
|
|
118
|
+
|
|
119
|
+
# Act
|
|
120
|
+
defaults = get_port_integration_defaults(
|
|
121
|
+
port_app_config_class=PortAppConfig,
|
|
122
|
+
custom_defaults_dir=".port/custom_resources",
|
|
123
|
+
base_path=custom_resources_dir.parent.parent,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Assert
|
|
127
|
+
assert isinstance(defaults, Defaults)
|
|
128
|
+
assert defaults.blueprints[0].get("identifier") == "mock-custom-identifier"
|
|
129
|
+
assert defaults.port_app_config is not None
|
|
130
|
+
assert defaults.port_app_config.resources[0].kind == "mock-custom-kind"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_fallback_to_default_dir_if_custom_dir_invalid(
|
|
134
|
+
setup_mock_directories: tuple[Path, Path, Path]
|
|
135
|
+
) -> None:
|
|
136
|
+
resources_dir, _, non_existing_dir = setup_mock_directories
|
|
137
|
+
|
|
138
|
+
# Arrange
|
|
139
|
+
with (
|
|
140
|
+
patch("port_ocean.core.defaults.common.is_valid_dir") as mock_is_valid_dir,
|
|
141
|
+
patch("pathlib.Path.iterdir", return_value=resources_dir.iterdir()),
|
|
142
|
+
):
|
|
143
|
+
|
|
144
|
+
mock_is_valid_dir.side_effect = lambda path: path == resources_dir
|
|
145
|
+
|
|
146
|
+
# Act
|
|
147
|
+
custom_defaults_dir = str(non_existing_dir.relative_to(resources_dir.parent))
|
|
148
|
+
defaults = get_port_integration_defaults(
|
|
149
|
+
port_app_config_class=PortAppConfig,
|
|
150
|
+
custom_defaults_dir=custom_defaults_dir,
|
|
151
|
+
base_path=resources_dir.parent.parent,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Assert
|
|
155
|
+
assert isinstance(defaults, Defaults)
|
|
156
|
+
assert defaults.blueprints[0].get("identifier") == "mock-identifier"
|
|
157
|
+
assert defaults.port_app_config is not None
|
|
158
|
+
assert defaults.port_app_config.resources[0].kind == "mock-kind"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_default_resources_path_does_not_exist() -> None:
|
|
162
|
+
# Act
|
|
163
|
+
defaults = get_port_integration_defaults(port_app_config_class=PortAppConfig)
|
|
164
|
+
|
|
165
|
+
# Assert
|
|
166
|
+
assert defaults is None
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from unittest.mock import AsyncMock, Mock
|
|
3
|
+
from loguru import logger
|
|
4
|
+
import pytest
|
|
5
|
+
from io import StringIO
|
|
6
|
+
|
|
7
|
+
from port_ocean.context.ocean import PortOceanContext
|
|
8
|
+
from port_ocean.core.handlers.entity_processor.jq_entity_processor import (
|
|
9
|
+
JQEntityProcessor,
|
|
10
|
+
)
|
|
11
|
+
from port_ocean.core.ocean_types import CalculationResult
|
|
12
|
+
from port_ocean.exceptions.core import EntityProcessorException
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
class TestJQEntityProcessor:
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mocked_processor(self, monkeypatch: Any) -> JQEntityProcessor:
|
|
20
|
+
mock_context = AsyncMock()
|
|
21
|
+
monkeypatch.setattr(PortOceanContext, "app", mock_context)
|
|
22
|
+
return JQEntityProcessor(mock_context)
|
|
23
|
+
|
|
24
|
+
async def test_compile(self, mocked_processor: JQEntityProcessor) -> None:
|
|
25
|
+
pattern = ".foo"
|
|
26
|
+
compiled = mocked_processor._compile(pattern)
|
|
27
|
+
assert compiled is not None
|
|
28
|
+
|
|
29
|
+
async def test_search(self, mocked_processor: JQEntityProcessor) -> None:
|
|
30
|
+
data = {"foo": "bar"}
|
|
31
|
+
pattern = ".foo"
|
|
32
|
+
result = await mocked_processor._search(data, pattern)
|
|
33
|
+
assert result == "bar"
|
|
34
|
+
|
|
35
|
+
async def test_search_as_bool(self, mocked_processor: JQEntityProcessor) -> None:
|
|
36
|
+
data = {"foo": True}
|
|
37
|
+
pattern = ".foo"
|
|
38
|
+
result = await mocked_processor._search_as_bool(data, pattern)
|
|
39
|
+
assert result is True
|
|
40
|
+
|
|
41
|
+
async def test_search_as_object(self, mocked_processor: JQEntityProcessor) -> None:
|
|
42
|
+
data = {"foo": {"bar": "baz"}}
|
|
43
|
+
obj = {"foo": ".foo.bar"}
|
|
44
|
+
result = await mocked_processor._search_as_object(data, obj)
|
|
45
|
+
assert result == {"foo": "baz"}
|
|
46
|
+
|
|
47
|
+
async def test_get_mapped_entity(self, mocked_processor: JQEntityProcessor) -> None:
|
|
48
|
+
data = {"foo": "bar"}
|
|
49
|
+
raw_entity_mappings = {"foo": ".foo"}
|
|
50
|
+
selector_query = '.foo == "bar"'
|
|
51
|
+
result = await mocked_processor._get_mapped_entity(
|
|
52
|
+
data, raw_entity_mappings, selector_query
|
|
53
|
+
)
|
|
54
|
+
assert result.entity == {"foo": "bar"}
|
|
55
|
+
assert result.did_entity_pass_selector is True
|
|
56
|
+
|
|
57
|
+
async def test_calculate_entity(self, mocked_processor: JQEntityProcessor) -> None:
|
|
58
|
+
data = {"foo": "bar"}
|
|
59
|
+
raw_entity_mappings = {"foo": ".foo"}
|
|
60
|
+
selector_query = '.foo == "bar"'
|
|
61
|
+
result, errors = await mocked_processor._calculate_entity(
|
|
62
|
+
data, raw_entity_mappings, None, selector_query
|
|
63
|
+
)
|
|
64
|
+
assert len(result) == 1
|
|
65
|
+
assert result[0].entity == {"foo": "bar"}
|
|
66
|
+
assert result[0].did_entity_pass_selector is True
|
|
67
|
+
assert not errors
|
|
68
|
+
|
|
69
|
+
async def test_parse_items(self, mocked_processor: JQEntityProcessor) -> None:
|
|
70
|
+
mapping = Mock()
|
|
71
|
+
mapping.port.entity.mappings.dict.return_value = {
|
|
72
|
+
"identifier": ".foo",
|
|
73
|
+
"blueprint": ".foo",
|
|
74
|
+
"properties": {"foo": ".foo"},
|
|
75
|
+
}
|
|
76
|
+
mapping.port.items_to_parse = None
|
|
77
|
+
mapping.selector.query = '.foo == "bar"'
|
|
78
|
+
raw_results = [{"foo": "bar"}]
|
|
79
|
+
result = await mocked_processor._parse_items(mapping, raw_results)
|
|
80
|
+
assert isinstance(result, CalculationResult)
|
|
81
|
+
assert len(result.entity_selector_diff.passed) == 1
|
|
82
|
+
assert result.entity_selector_diff.passed[0].properties.get("foo") == "bar"
|
|
83
|
+
assert not result.errors
|
|
84
|
+
|
|
85
|
+
async def test_in_operator(self, mocked_processor: JQEntityProcessor) -> None:
|
|
86
|
+
data = {
|
|
87
|
+
"key": "GetPort_SelfService",
|
|
88
|
+
"name": "GetPort SelfService",
|
|
89
|
+
"desc": "Test",
|
|
90
|
+
"qualifier": "VW",
|
|
91
|
+
"visibility": "public",
|
|
92
|
+
"selectionMode": "NONE",
|
|
93
|
+
"subViews": [
|
|
94
|
+
{
|
|
95
|
+
"key": "GetPort_SelfService_Second",
|
|
96
|
+
"name": "GetPort SelfService Second",
|
|
97
|
+
"qualifier": "SVW",
|
|
98
|
+
"selectionMode": "NONE",
|
|
99
|
+
"subViews": [
|
|
100
|
+
{
|
|
101
|
+
"key": "GetPort_SelfService_Third",
|
|
102
|
+
"name": "GetPort SelfService Third",
|
|
103
|
+
"qualifier": "SVW",
|
|
104
|
+
"selectionMode": "NONE",
|
|
105
|
+
"subViews": [],
|
|
106
|
+
"referencedBy": [],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"key": "Port_Test",
|
|
110
|
+
"name": "Port Test",
|
|
111
|
+
"qualifier": "SVW",
|
|
112
|
+
"selectionMode": "NONE",
|
|
113
|
+
"subViews": [],
|
|
114
|
+
"referencedBy": [],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
"referencedBy": [],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"key": "Python",
|
|
121
|
+
"name": "Python",
|
|
122
|
+
"qualifier": "SVW",
|
|
123
|
+
"selectionMode": "NONE",
|
|
124
|
+
"subViews": [
|
|
125
|
+
{
|
|
126
|
+
"key": "Time",
|
|
127
|
+
"name": "Time",
|
|
128
|
+
"qualifier": "SVW",
|
|
129
|
+
"selectionMode": "NONE",
|
|
130
|
+
"subViews": [
|
|
131
|
+
{
|
|
132
|
+
"key": "port_*****",
|
|
133
|
+
"name": "port-*****",
|
|
134
|
+
"qualifier": "SVW",
|
|
135
|
+
"selectionMode": "NONE",
|
|
136
|
+
"subViews": [
|
|
137
|
+
{
|
|
138
|
+
"key": "port_*****:REferenced",
|
|
139
|
+
"name": "REferenced",
|
|
140
|
+
"qualifier": "VW",
|
|
141
|
+
"visibility": "public",
|
|
142
|
+
"originalKey": "REferenced",
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"referencedBy": [],
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"referencedBy": [],
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"referencedBy": [],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"key": "GetPort_SelfService:Authentication_Application",
|
|
155
|
+
"name": "Authentication Application",
|
|
156
|
+
"desc": "For auth services",
|
|
157
|
+
"qualifier": "APP",
|
|
158
|
+
"visibility": "private",
|
|
159
|
+
"selectedBranches": ["main"],
|
|
160
|
+
"originalKey": "Authentication_Application",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
"referencedBy": [],
|
|
164
|
+
}
|
|
165
|
+
pattern = '.subViews | map(select((.qualifier | IN("VW", "SVW"))) | .key)'
|
|
166
|
+
result = await mocked_processor._search(data, pattern)
|
|
167
|
+
assert result == ["GetPort_SelfService_Second", "Python"]
|
|
168
|
+
|
|
169
|
+
async def test_failure_of_jq_expression(
|
|
170
|
+
self, mocked_processor: JQEntityProcessor
|
|
171
|
+
) -> None:
|
|
172
|
+
data = {"foo": "bar"}
|
|
173
|
+
pattern = ".foo."
|
|
174
|
+
result = await mocked_processor._search(data, pattern)
|
|
175
|
+
assert result is None
|
|
176
|
+
|
|
177
|
+
async def test_search_as_object_failure(
|
|
178
|
+
self, mocked_processor: JQEntityProcessor
|
|
179
|
+
) -> None:
|
|
180
|
+
data = {"foo": {"bar": "baz"}}
|
|
181
|
+
obj = {"foo": ".foo.bar."}
|
|
182
|
+
result = await mocked_processor._search_as_object(data, obj)
|
|
183
|
+
assert result == {"foo": None}
|
|
184
|
+
|
|
185
|
+
async def test_double_quotes_in_jq_expression(
|
|
186
|
+
self, mocked_processor: JQEntityProcessor
|
|
187
|
+
) -> None:
|
|
188
|
+
data = {"foo": "bar"}
|
|
189
|
+
pattern = '"shalom"'
|
|
190
|
+
result = await mocked_processor._search(data, pattern)
|
|
191
|
+
assert result == "shalom"
|
|
192
|
+
|
|
193
|
+
async def test_search_as_bool_failure(
|
|
194
|
+
self, mocked_processor: JQEntityProcessor
|
|
195
|
+
) -> None:
|
|
196
|
+
data = {"foo": "bar"}
|
|
197
|
+
pattern = ".foo"
|
|
198
|
+
with pytest.raises(
|
|
199
|
+
EntityProcessorException,
|
|
200
|
+
match="Expected boolean value, got value:bar of type: <class 'str'> instead",
|
|
201
|
+
):
|
|
202
|
+
await mocked_processor._search_as_bool(data, pattern)
|
|
203
|
+
|
|
204
|
+
@pytest.mark.parametrize(
|
|
205
|
+
"pattern, expected",
|
|
206
|
+
[
|
|
207
|
+
('.parameters[] | select(.name == "not_exists") | .value', None),
|
|
208
|
+
(
|
|
209
|
+
'.parameters[] | select(.name == "parameter_name") | .value',
|
|
210
|
+
"parameter_value",
|
|
211
|
+
),
|
|
212
|
+
(
|
|
213
|
+
'.parameters[] | select(.name == "another_parameter") | .value',
|
|
214
|
+
"another_value",
|
|
215
|
+
),
|
|
216
|
+
],
|
|
217
|
+
)
|
|
218
|
+
async def test_search_fails_on_stop_iteration(
|
|
219
|
+
self, mocked_processor: JQEntityProcessor, pattern: str, expected: Any
|
|
220
|
+
) -> None:
|
|
221
|
+
data = {
|
|
222
|
+
"parameters": [
|
|
223
|
+
{"name": "parameter_name", "value": "parameter_value"},
|
|
224
|
+
{"name": "another_parameter", "value": "another_value"},
|
|
225
|
+
{"name": "another_parameter", "value": "another_value2"},
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
result = await mocked_processor._search(data, pattern)
|
|
229
|
+
assert result == expected
|
|
230
|
+
|
|
231
|
+
async def test_return_a_list_of_values(
|
|
232
|
+
self, mocked_processor: JQEntityProcessor
|
|
233
|
+
) -> None:
|
|
234
|
+
data = {"parameters": ["parameter_value", "another_value", "another_value2"]}
|
|
235
|
+
pattern = ".parameters"
|
|
236
|
+
result = await mocked_processor._search(data, pattern)
|
|
237
|
+
assert result == ["parameter_value", "another_value", "another_value2"]
|
|
238
|
+
|
|
239
|
+
@pytest.mark.timeout(3)
|
|
240
|
+
async def test_search_performance_10000(
|
|
241
|
+
self, mocked_processor: JQEntityProcessor
|
|
242
|
+
) -> None:
|
|
243
|
+
"""
|
|
244
|
+
This test is to check the performance of the search method when called 10000 times.
|
|
245
|
+
"""
|
|
246
|
+
data = {"foo": "bar"}
|
|
247
|
+
pattern = ".foo"
|
|
248
|
+
for _ in range(10000):
|
|
249
|
+
result = await mocked_processor._search(data, pattern)
|
|
250
|
+
assert result == "bar"
|
|
251
|
+
|
|
252
|
+
@pytest.mark.timeout(15)
|
|
253
|
+
async def test_parse_items_performance_10000(
|
|
254
|
+
self, mocked_processor: JQEntityProcessor
|
|
255
|
+
) -> None:
|
|
256
|
+
"""
|
|
257
|
+
This test is to check the performance of the parse_items method when called 10000 times.
|
|
258
|
+
"""
|
|
259
|
+
mapping = Mock()
|
|
260
|
+
mapping.port.entity.mappings.dict.return_value = {
|
|
261
|
+
"identifier": ".foo",
|
|
262
|
+
"blueprint": ".foo",
|
|
263
|
+
"properties": {"foo": ".foo"},
|
|
264
|
+
}
|
|
265
|
+
mapping.port.items_to_parse = None
|
|
266
|
+
mapping.selector.query = '.foo == "bar"'
|
|
267
|
+
raw_results = [{"foo": "bar"}]
|
|
268
|
+
for _ in range(10000):
|
|
269
|
+
result = await mocked_processor._parse_items(mapping, raw_results)
|
|
270
|
+
assert isinstance(result, CalculationResult)
|
|
271
|
+
assert len(result.entity_selector_diff.passed) == 1
|
|
272
|
+
assert result.entity_selector_diff.passed[0].properties.get("foo") == "bar"
|
|
273
|
+
assert not result.errors
|
|
274
|
+
|
|
275
|
+
async def test_parse_items_wrong_mapping(
|
|
276
|
+
self, mocked_processor: JQEntityProcessor
|
|
277
|
+
) -> None:
|
|
278
|
+
mapping = Mock()
|
|
279
|
+
mapping.port.entity.mappings.dict.return_value = {
|
|
280
|
+
"title": ".foo",
|
|
281
|
+
"identifier": ".ark",
|
|
282
|
+
"blueprint": ".baz",
|
|
283
|
+
"properties": {
|
|
284
|
+
"description": ".bazbar",
|
|
285
|
+
"url": ".foobar",
|
|
286
|
+
"defaultBranch": ".bar.baz",
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
mapping.port.items_to_parse = None
|
|
290
|
+
mapping.selector.query = "true"
|
|
291
|
+
raw_results = [
|
|
292
|
+
{
|
|
293
|
+
"foo": "bar",
|
|
294
|
+
"baz": "bazbar",
|
|
295
|
+
"bar": {"foobar": "barfoo", "baz": "barbaz"},
|
|
296
|
+
},
|
|
297
|
+
{"foo": "bar", "baz": "bazbar", "bar": {"foobar": "foobar"}},
|
|
298
|
+
]
|
|
299
|
+
result = await mocked_processor._parse_items(mapping, raw_results)
|
|
300
|
+
assert len(result.misonfigured_entity_keys) > 0
|
|
301
|
+
assert len(result.misonfigured_entity_keys) == 4
|
|
302
|
+
assert result.misonfigured_entity_keys == {
|
|
303
|
+
"identifier": ".ark",
|
|
304
|
+
"description": ".bazbar",
|
|
305
|
+
"url": ".foobar",
|
|
306
|
+
"defaultBranch": ".bar.baz",
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async def test_parse_items_empty_required(
|
|
310
|
+
self, mocked_processor: JQEntityProcessor
|
|
311
|
+
) -> None:
|
|
312
|
+
stream = StringIO()
|
|
313
|
+
sink_id = logger.add(stream, level="DEBUG")
|
|
314
|
+
|
|
315
|
+
mapping = Mock()
|
|
316
|
+
mapping.port.entity.mappings.dict.return_value = {
|
|
317
|
+
"identifier": ".foo",
|
|
318
|
+
"blueprint": ".bar",
|
|
319
|
+
}
|
|
320
|
+
mapping.port.items_to_parse = None
|
|
321
|
+
mapping.selector.query = "true"
|
|
322
|
+
raw_results: list[dict[Any, Any]] = [
|
|
323
|
+
{"foo": "", "bar": "bluePrintMapped"},
|
|
324
|
+
{"foo": "identifierMapped", "bar": ""},
|
|
325
|
+
]
|
|
326
|
+
result = await mocked_processor._parse_items(mapping, raw_results)
|
|
327
|
+
assert "identifier" not in result.misonfigured_entity_keys
|
|
328
|
+
assert "blueprint" not in result.misonfigured_entity_keys
|
|
329
|
+
|
|
330
|
+
raw_results = [
|
|
331
|
+
{"foo": "identifierMapped", "bar": None},
|
|
332
|
+
{"foo": None, "bar": ""},
|
|
333
|
+
]
|
|
334
|
+
result = await mocked_processor._parse_items(mapping, raw_results)
|
|
335
|
+
assert result.misonfigured_entity_keys == {
|
|
336
|
+
"identifier": ".foo",
|
|
337
|
+
"blueprint": ".bar",
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
logger.remove(sink_id)
|
|
341
|
+
logs_captured = stream.getvalue()
|
|
342
|
+
|
|
343
|
+
assert (
|
|
344
|
+
"2 transformations of batch failed due to empty, null or missing values"
|
|
345
|
+
in logs_captured
|
|
346
|
+
)
|
|
347
|
+
assert (
|
|
348
|
+
"{'blueprint': '.bar', 'identifier': '.foo'} (null, missing, or misconfigured)"
|
|
349
|
+
in logs_captured
|
|
350
|
+
)
|