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.

Files changed (112) hide show
  1. integrations/_infra/Dockerfile.Deb +56 -0
  2. integrations/_infra/Dockerfile.alpine +108 -0
  3. integrations/_infra/Dockerfile.base.builder +26 -0
  4. integrations/_infra/Dockerfile.base.runner +13 -0
  5. integrations/_infra/Dockerfile.dockerignore +94 -0
  6. {port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}} → integrations/_infra}/Makefile +21 -8
  7. integrations/_infra/grpcio.sh +18 -0
  8. integrations/_infra/init.sh +5 -0
  9. port_ocean/bootstrap.py +1 -1
  10. port_ocean/cli/commands/defaults/clean.py +3 -1
  11. port_ocean/cli/commands/new.py +42 -7
  12. port_ocean/cli/commands/sail.py +7 -1
  13. port_ocean/cli/cookiecutter/cookiecutter.json +3 -0
  14. port_ocean/cli/cookiecutter/hooks/post_gen_project.py +20 -3
  15. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +6 -0
  16. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +41 -0
  17. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +16 -0
  18. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +6 -7
  19. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +1 -1
  20. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +7 -0
  21. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +1 -0
  22. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +16 -1
  23. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +21 -10
  24. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +2 -0
  25. port_ocean/clients/port/authentication.py +16 -4
  26. port_ocean/clients/port/client.py +17 -0
  27. port_ocean/clients/port/mixins/blueprints.py +7 -8
  28. port_ocean/clients/port/mixins/entities.py +108 -53
  29. port_ocean/clients/port/mixins/integrations.py +23 -34
  30. port_ocean/clients/port/retry_transport.py +0 -5
  31. port_ocean/clients/port/utils.py +9 -3
  32. port_ocean/config/base.py +16 -16
  33. port_ocean/config/dynamic.py +2 -0
  34. port_ocean/config/settings.py +79 -11
  35. port_ocean/context/event.py +18 -5
  36. port_ocean/context/ocean.py +14 -3
  37. port_ocean/core/defaults/clean.py +10 -3
  38. port_ocean/core/defaults/common.py +25 -9
  39. port_ocean/core/defaults/initialize.py +111 -100
  40. port_ocean/core/event_listener/__init__.py +8 -0
  41. port_ocean/core/event_listener/base.py +49 -10
  42. port_ocean/core/event_listener/factory.py +9 -1
  43. port_ocean/core/event_listener/http.py +11 -3
  44. port_ocean/core/event_listener/kafka.py +24 -5
  45. port_ocean/core/event_listener/once.py +96 -4
  46. port_ocean/core/event_listener/polling.py +16 -14
  47. port_ocean/core/event_listener/webhooks_only.py +41 -0
  48. port_ocean/core/handlers/__init__.py +1 -2
  49. port_ocean/core/handlers/entities_state_applier/base.py +4 -1
  50. port_ocean/core/handlers/entities_state_applier/port/applier.py +29 -87
  51. port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +5 -2
  52. port_ocean/core/handlers/entity_processor/base.py +26 -22
  53. port_ocean/core/handlers/entity_processor/jq_entity_processor.py +253 -45
  54. port_ocean/core/handlers/port_app_config/base.py +55 -15
  55. port_ocean/core/handlers/port_app_config/models.py +24 -5
  56. port_ocean/core/handlers/resync_state_updater/__init__.py +5 -0
  57. port_ocean/core/handlers/resync_state_updater/updater.py +84 -0
  58. port_ocean/core/integrations/base.py +5 -7
  59. port_ocean/core/integrations/mixins/events.py +3 -1
  60. port_ocean/core/integrations/mixins/sync.py +4 -2
  61. port_ocean/core/integrations/mixins/sync_raw.py +209 -74
  62. port_ocean/core/integrations/mixins/utils.py +1 -1
  63. port_ocean/core/models.py +44 -0
  64. port_ocean/core/ocean_types.py +29 -11
  65. port_ocean/core/utils/entity_topological_sorter.py +90 -0
  66. port_ocean/core/utils/utils.py +109 -0
  67. port_ocean/debug_cli.py +5 -0
  68. port_ocean/exceptions/core.py +4 -0
  69. port_ocean/exceptions/port_defaults.py +0 -2
  70. port_ocean/helpers/retry.py +85 -24
  71. port_ocean/log/handlers.py +23 -2
  72. port_ocean/log/logger_setup.py +8 -1
  73. port_ocean/log/sensetive.py +25 -10
  74. port_ocean/middlewares.py +10 -2
  75. port_ocean/ocean.py +57 -24
  76. port_ocean/run.py +10 -5
  77. port_ocean/tests/__init__.py +0 -0
  78. port_ocean/tests/clients/port/mixins/test_entities.py +53 -0
  79. port_ocean/tests/conftest.py +4 -0
  80. port_ocean/tests/core/defaults/test_common.py +166 -0
  81. port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +350 -0
  82. port_ocean/tests/core/handlers/mixins/test_sync_raw.py +552 -0
  83. port_ocean/tests/core/test_utils.py +73 -0
  84. port_ocean/tests/core/utils/test_entity_topological_sorter.py +99 -0
  85. port_ocean/tests/helpers/__init__.py +0 -0
  86. port_ocean/tests/helpers/fake_port_api.py +191 -0
  87. port_ocean/tests/helpers/fixtures.py +46 -0
  88. port_ocean/tests/helpers/integration.py +31 -0
  89. port_ocean/tests/helpers/ocean_app.py +66 -0
  90. port_ocean/tests/helpers/port_client.py +21 -0
  91. port_ocean/tests/helpers/smoke_test.py +82 -0
  92. port_ocean/tests/log/test_handlers.py +71 -0
  93. port_ocean/tests/test_smoke.py +74 -0
  94. port_ocean/tests/utils/test_async_iterators.py +45 -0
  95. port_ocean/tests/utils/test_cache.py +189 -0
  96. port_ocean/utils/async_iterators.py +109 -0
  97. port_ocean/utils/cache.py +37 -1
  98. port_ocean/utils/misc.py +22 -4
  99. port_ocean/utils/queue_utils.py +88 -0
  100. port_ocean/utils/signal.py +1 -4
  101. port_ocean/utils/time.py +54 -0
  102. {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/METADATA +27 -19
  103. port_ocean-0.17.8.dist-info/RECORD +164 -0
  104. {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/WHEEL +1 -1
  105. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -94
  106. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -15
  107. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -17
  108. port_ocean/core/handlers/entities_state_applier/port/validate_entity_relations.py +0 -40
  109. port_ocean/core/utils.py +0 -65
  110. port_ocean-0.5.5.dist-info/RECORD +0 -129
  111. {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/LICENSE.md +0 -0
  112. {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,4 @@
1
+ # ruff: noqa
2
+ from port_ocean.tests.helpers.fixtures import (
3
+ port_client_for_fake_integration,
4
+ )
@@ -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
+ )