port-ocean 0.18.4__py3-none-any.whl → 0.18.5__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.
- port_ocean/context/event.py +8 -1
- port_ocean/core/handlers/port_app_config/api.py +4 -1
- port_ocean/exceptions/api.py +7 -0
- port_ocean/tests/core/handlers/port_app_config/test_api.py +67 -0
- port_ocean/tests/core/handlers/port_app_config/test_base.py +197 -0
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.5.dist-info}/METADATA +1 -1
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.5.dist-info}/RECORD +10 -8
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.5.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.5.dist-info}/WHEEL +0 -0
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.5.dist-info}/entry_points.txt +0 -0
port_ocean/context/event.py
CHANGED
@@ -19,6 +19,7 @@ from pydispatch import dispatcher # type: ignore
|
|
19
19
|
from werkzeug.local import LocalStack, LocalProxy
|
20
20
|
|
21
21
|
from port_ocean.context.resource import resource
|
22
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
22
23
|
from port_ocean.exceptions.context import (
|
23
24
|
EventContextNotFoundError,
|
24
25
|
ResourceContextNotFoundError,
|
@@ -176,8 +177,14 @@ async def event_context(
|
|
176
177
|
logger.info("Event started")
|
177
178
|
try:
|
178
179
|
yield event
|
179
|
-
except:
|
180
|
+
except EmptyPortAppConfigError as e:
|
181
|
+
logger.error(
|
182
|
+
f"Skipping resync due to empty mapping: {str(e)}", exc_info=True
|
183
|
+
)
|
184
|
+
raise
|
185
|
+
except Exception as e:
|
180
186
|
success = False
|
187
|
+
logger.error(f"Event failed with error: {str(e)}", exc_info=True)
|
181
188
|
raise
|
182
189
|
else:
|
183
190
|
success = True
|
@@ -3,6 +3,7 @@ from typing import Any
|
|
3
3
|
from loguru import logger
|
4
4
|
|
5
5
|
from port_ocean.core.handlers.port_app_config.base import BasePortAppConfig
|
6
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
6
7
|
|
7
8
|
|
8
9
|
class APIPortAppConfig(BasePortAppConfig):
|
@@ -20,7 +21,9 @@ class APIPortAppConfig(BasePortAppConfig):
|
|
20
21
|
if not config:
|
21
22
|
logger.error(
|
22
23
|
"The integration port app config is empty. "
|
24
|
+
f"Integration: {integration}, "
|
25
|
+
f"Config: {config}. "
|
23
26
|
"Please make sure to configure your port app config using Port's API."
|
24
27
|
)
|
25
|
-
|
28
|
+
raise EmptyPortAppConfigError()
|
26
29
|
return config
|
port_ocean/exceptions/api.py
CHANGED
@@ -13,3 +13,10 @@ class BaseAPIException(BaseOceanException, abc.ABC):
|
|
13
13
|
class InternalServerException(BaseAPIException):
|
14
14
|
def response(self) -> Response:
|
15
15
|
return PlainTextResponse(content="Internal server error", status_code=500)
|
16
|
+
|
17
|
+
|
18
|
+
class EmptyPortAppConfigError(Exception):
|
19
|
+
"""Exception raised when the Port app configuration is empty."""
|
20
|
+
|
21
|
+
def __init__(self, message: str = "Port app config is empty") -> None:
|
22
|
+
super().__init__(message)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import AsyncMock
|
3
|
+
|
4
|
+
from port_ocean.core.handlers.port_app_config.api import APIPortAppConfig
|
5
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.fixture
|
9
|
+
def mock_context() -> AsyncMock:
|
10
|
+
context = AsyncMock()
|
11
|
+
context.port_client.get_current_integration = AsyncMock()
|
12
|
+
return context
|
13
|
+
|
14
|
+
|
15
|
+
@pytest.fixture
|
16
|
+
def api_config(mock_context: AsyncMock) -> APIPortAppConfig:
|
17
|
+
return APIPortAppConfig(mock_context)
|
18
|
+
|
19
|
+
|
20
|
+
async def test_get_port_app_config_valid_config_returns_config(
|
21
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
22
|
+
) -> None:
|
23
|
+
# Arrange
|
24
|
+
expected_config = {"key": "value"}
|
25
|
+
mock_context.port_client.get_current_integration.return_value = {
|
26
|
+
"config": expected_config
|
27
|
+
}
|
28
|
+
|
29
|
+
# Act
|
30
|
+
result = await api_config._get_port_app_config()
|
31
|
+
|
32
|
+
# Assert
|
33
|
+
assert result == expected_config
|
34
|
+
mock_context.port_client.get_current_integration.assert_called_once()
|
35
|
+
|
36
|
+
|
37
|
+
async def test_get_port_app_config_empty_config_raises_value_error(
|
38
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
39
|
+
) -> None:
|
40
|
+
# Arrange
|
41
|
+
mock_context.port_client.get_current_integration.return_value = {"config": {}}
|
42
|
+
|
43
|
+
# Act & Assert
|
44
|
+
with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"):
|
45
|
+
await api_config._get_port_app_config()
|
46
|
+
|
47
|
+
|
48
|
+
async def test_get_port_app_config_missing_config_key_raises_key_error(
|
49
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
50
|
+
) -> None:
|
51
|
+
# Arrange
|
52
|
+
mock_context.port_client.get_current_integration.return_value = {}
|
53
|
+
|
54
|
+
# Act & Assert
|
55
|
+
with pytest.raises(KeyError):
|
56
|
+
await api_config._get_port_app_config()
|
57
|
+
|
58
|
+
|
59
|
+
async def test_get_port_app_config_empty_integration_raises_key_error(
|
60
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
61
|
+
) -> None:
|
62
|
+
# Arrange
|
63
|
+
mock_context.port_client.get_current_integration.return_value = {}
|
64
|
+
|
65
|
+
# Act & Assert
|
66
|
+
with pytest.raises(KeyError):
|
67
|
+
await api_config._get_port_app_config()
|
@@ -0,0 +1,197 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import MagicMock
|
3
|
+
from pydantic import ValidationError
|
4
|
+
from typing import Any, Dict
|
5
|
+
|
6
|
+
from port_ocean.context.ocean import PortOceanContext
|
7
|
+
from port_ocean.core.handlers.port_app_config.base import BasePortAppConfig
|
8
|
+
from port_ocean.core.handlers.port_app_config.models import PortAppConfig
|
9
|
+
from port_ocean.context.event import EventType, event_context
|
10
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
11
|
+
|
12
|
+
|
13
|
+
class TestPortAppConfig(BasePortAppConfig):
|
14
|
+
mock_get_port_app_config: Any
|
15
|
+
|
16
|
+
async def _get_port_app_config(self) -> Dict[str, Any]:
|
17
|
+
return self.mock_get_port_app_config()
|
18
|
+
|
19
|
+
|
20
|
+
@pytest.fixture
|
21
|
+
def mock_context() -> PortOceanContext:
|
22
|
+
context = MagicMock(spec=PortOceanContext)
|
23
|
+
context.config.port.port_app_config_cache_ttl = 300 # 5 minutes
|
24
|
+
return context
|
25
|
+
|
26
|
+
|
27
|
+
@pytest.fixture
|
28
|
+
def port_app_config_handler(mock_context: PortOceanContext) -> TestPortAppConfig:
|
29
|
+
handler = TestPortAppConfig(mock_context)
|
30
|
+
handler.mock_get_port_app_config = MagicMock()
|
31
|
+
return handler
|
32
|
+
|
33
|
+
|
34
|
+
@pytest.mark.asyncio
|
35
|
+
async def test_get_port_app_config_success(
|
36
|
+
port_app_config_handler: TestPortAppConfig,
|
37
|
+
) -> None:
|
38
|
+
# Arrange
|
39
|
+
valid_config = {
|
40
|
+
"resources": [
|
41
|
+
{
|
42
|
+
"kind": "repository",
|
43
|
+
"selector": {"query": "true"},
|
44
|
+
"port": {
|
45
|
+
"entity": {
|
46
|
+
"mappings": {
|
47
|
+
"identifier": ".name",
|
48
|
+
"title": ".name",
|
49
|
+
"blueprint": '"service"',
|
50
|
+
"properties": {
|
51
|
+
"description": ".description",
|
52
|
+
"url": ".html_url",
|
53
|
+
"defaultBranch": ".default_branch",
|
54
|
+
},
|
55
|
+
}
|
56
|
+
}
|
57
|
+
},
|
58
|
+
}
|
59
|
+
]
|
60
|
+
}
|
61
|
+
port_app_config_handler.mock_get_port_app_config.return_value = valid_config
|
62
|
+
|
63
|
+
# Act
|
64
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
65
|
+
result = await port_app_config_handler.get_port_app_config()
|
66
|
+
|
67
|
+
# Assert
|
68
|
+
assert isinstance(result, PortAppConfig)
|
69
|
+
assert result.resources[0].port.entity.mappings.title == ".name"
|
70
|
+
assert result.resources[0].port.entity.mappings.identifier == ".name"
|
71
|
+
assert result.resources[0].port.entity.mappings.blueprint == '"service"'
|
72
|
+
assert (
|
73
|
+
result.resources[0].port.entity.mappings.properties["description"]
|
74
|
+
== ".description"
|
75
|
+
)
|
76
|
+
assert result.resources[0].port.entity.mappings.properties["url"] == ".html_url"
|
77
|
+
assert (
|
78
|
+
result.resources[0].port.entity.mappings.properties["defaultBranch"]
|
79
|
+
== ".default_branch"
|
80
|
+
)
|
81
|
+
port_app_config_handler.mock_get_port_app_config.assert_called_once()
|
82
|
+
|
83
|
+
|
84
|
+
@pytest.mark.asyncio
|
85
|
+
async def test_get_port_app_config_uses_cache(
|
86
|
+
port_app_config_handler: TestPortAppConfig,
|
87
|
+
) -> None:
|
88
|
+
# Arrange
|
89
|
+
valid_config = {
|
90
|
+
"resources": [
|
91
|
+
{
|
92
|
+
"kind": "repository",
|
93
|
+
"selector": {"query": "true"},
|
94
|
+
"port": {
|
95
|
+
"entity": {
|
96
|
+
"mappings": {
|
97
|
+
"identifier": ".name",
|
98
|
+
"title": ".name",
|
99
|
+
"blueprint": '"service"',
|
100
|
+
"properties": {
|
101
|
+
"description": ".description",
|
102
|
+
"url": ".html_url",
|
103
|
+
"defaultBranch": ".default_branch",
|
104
|
+
},
|
105
|
+
}
|
106
|
+
}
|
107
|
+
},
|
108
|
+
}
|
109
|
+
]
|
110
|
+
}
|
111
|
+
port_app_config_handler.mock_get_port_app_config.return_value = valid_config
|
112
|
+
|
113
|
+
# Act
|
114
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
115
|
+
result1 = await port_app_config_handler.get_port_app_config()
|
116
|
+
result2 = await port_app_config_handler.get_port_app_config()
|
117
|
+
|
118
|
+
# Assert
|
119
|
+
assert result1 == result2
|
120
|
+
port_app_config_handler.mock_get_port_app_config.assert_called_once() # Called only once due to caching
|
121
|
+
|
122
|
+
|
123
|
+
@pytest.mark.asyncio
|
124
|
+
async def test_get_port_app_config_bypass_cache(
|
125
|
+
port_app_config_handler: TestPortAppConfig,
|
126
|
+
) -> None:
|
127
|
+
# Arrange
|
128
|
+
valid_config = {
|
129
|
+
"resources": [
|
130
|
+
{
|
131
|
+
"kind": "repository",
|
132
|
+
"selector": {"query": "true"},
|
133
|
+
"port": {
|
134
|
+
"entity": {
|
135
|
+
"mappings": {
|
136
|
+
"identifier": ".name",
|
137
|
+
"title": ".name",
|
138
|
+
"blueprint": '"service"',
|
139
|
+
"properties": {
|
140
|
+
"description": ".description",
|
141
|
+
"url": ".html_url",
|
142
|
+
"defaultBranch": ".default_branch",
|
143
|
+
},
|
144
|
+
}
|
145
|
+
}
|
146
|
+
},
|
147
|
+
}
|
148
|
+
]
|
149
|
+
}
|
150
|
+
port_app_config_handler.mock_get_port_app_config.return_value = valid_config
|
151
|
+
|
152
|
+
# Act
|
153
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
154
|
+
result1 = await port_app_config_handler.get_port_app_config()
|
155
|
+
result2 = await port_app_config_handler.get_port_app_config(use_cache=False)
|
156
|
+
|
157
|
+
# Assert
|
158
|
+
assert result1 == result2
|
159
|
+
assert (
|
160
|
+
port_app_config_handler.mock_get_port_app_config.call_count == 2
|
161
|
+
) # Called twice due to cache bypass
|
162
|
+
|
163
|
+
|
164
|
+
@pytest.mark.asyncio
|
165
|
+
async def test_get_port_app_config_validation_error(
|
166
|
+
port_app_config_handler: TestPortAppConfig, monkeypatch: pytest.MonkeyPatch
|
167
|
+
) -> None:
|
168
|
+
# Arrange
|
169
|
+
invalid_config = {"invalid_field": "invalid_value"}
|
170
|
+
port_app_config_handler.mock_get_port_app_config.return_value = invalid_config
|
171
|
+
|
172
|
+
def mock_parse_obj(*args: Any, **kwargs: Any) -> None:
|
173
|
+
raise ValidationError(errors=[], model=PortAppConfig)
|
174
|
+
|
175
|
+
monkeypatch.setattr(
|
176
|
+
port_app_config_handler.CONFIG_CLASS, "parse_obj", mock_parse_obj
|
177
|
+
)
|
178
|
+
|
179
|
+
# Act & Assert
|
180
|
+
with pytest.raises(ValidationError):
|
181
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
182
|
+
await port_app_config_handler.get_port_app_config()
|
183
|
+
|
184
|
+
|
185
|
+
@pytest.mark.asyncio
|
186
|
+
async def test_get_port_app_config_fetch_error(
|
187
|
+
port_app_config_handler: TestPortAppConfig,
|
188
|
+
) -> None:
|
189
|
+
# Arrange
|
190
|
+
port_app_config_handler.mock_get_port_app_config.side_effect = (
|
191
|
+
EmptyPortAppConfigError("Port app config is empty")
|
192
|
+
)
|
193
|
+
|
194
|
+
# Act & Assert
|
195
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
196
|
+
with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"):
|
197
|
+
await port_app_config_handler.get_port_app_config()
|
@@ -63,7 +63,7 @@ port_ocean/config/settings.py,sha256=q5KgDIr8snIxejoKyWSyf21R_AYEEXiEd3-ry9qf3ss
|
|
63
63
|
port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
64
|
port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
|
65
65
|
port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
66
|
-
port_ocean/context/event.py,sha256=
|
66
|
+
port_ocean/context/event.py,sha256=QK2ben4fJtxdorq_yRroATttP0DRc4wLtlUJ1as5D58,6208
|
67
67
|
port_ocean/context/ocean.py,sha256=0kgIi0zAsGarF52Qehu4bOxnAFEb0yayzG7xZioMHJc,4993
|
68
68
|
port_ocean/context/resource.py,sha256=yDj63URzQelj8zJPh4BAzTtPhpKr9Gw9DRn7I_0mJ1s,1692
|
69
69
|
port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -91,7 +91,7 @@ port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybe
|
|
91
91
|
port_ocean/core/handlers/entity_processor/base.py,sha256=udR0w5TstTOS5xOfTjAZIEdldn4xr6Oyb3DylatYX3Q,1869
|
92
92
|
port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=C6zJbS3miKyDeXiEV-0t5vJvkEznOeXRZFFOnwJcNdA,11714
|
93
93
|
port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iEgEeXtn2pRMrT4Dze5r1Ixbk,134
|
94
|
-
port_ocean/core/handlers/port_app_config/api.py,sha256=
|
94
|
+
port_ocean/core/handlers/port_app_config/api.py,sha256=r_Th66NEw38IpRdnXZcRvI8ACfvxW_A6V62WLwjWXlQ,1044
|
95
95
|
port_ocean/core/handlers/port_app_config/base.py,sha256=4Nxt2g8voEIHJ4Y1Km5NJcaG2iSbCklw5P8-Kus7Y9k,3007
|
96
96
|
port_ocean/core/handlers/port_app_config/models.py,sha256=YvYtf_44KD_rN4xK-3xHtdpRZ1M8Qo-m9K4LDtH7FYU,2344
|
97
97
|
port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
|
@@ -110,7 +110,7 @@ port_ocean/core/utils/entity_topological_sorter.py,sha256=MDUjM6OuDy4Xj68o-7InNN
|
|
110
110
|
port_ocean/core/utils/utils.py,sha256=HmumOeH27N0NX1_OP3t4oGKt074ht9XyXhvfZ5I05s4,6474
|
111
111
|
port_ocean/debug_cli.py,sha256=gHrv-Ey3cImKOcGZpjoHlo4pa_zfmyOl6TUM4o9VtcA,96
|
112
112
|
port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
113
|
-
port_ocean/exceptions/api.py,sha256=
|
113
|
+
port_ocean/exceptions/api.py,sha256=1JcA-H12lhSgolMEA6dM4JMbDrh9sYDcE7oydPSTuK8,649
|
114
114
|
port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
|
115
115
|
port_ocean/exceptions/clients.py,sha256=LKLLs-Zy3caNG85rwxfOw2rMr8qqVV6SHUq4fRCZ99U,180
|
116
116
|
port_ocean/exceptions/context.py,sha256=mA8HII6Rl4QxKUz98ppy1zX3kaziaen21h1ZWuU3ADc,372
|
@@ -136,6 +136,8 @@ port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,
|
|
136
136
|
port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
|
137
137
|
port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=FnEnaDjuoAbKvKyv6xJ46n3j0ZcaT70Sg2zc7oy7HAA,13596
|
138
138
|
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=SSf1ZRZVcIta9zfx2-SpYFc_-MoHPDJSa1MkbIx3icI,31172
|
139
|
+
port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
|
140
|
+
port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=s3D98JP3YV9V6T5PCDPE2852gkqGiDmo03UyexESX_I,6872
|
139
141
|
port_ocean/tests/core/test_utils.py,sha256=Z3kdhb5V7Svhcyy3EansdTpgHL36TL6erNtU-OPwAcI,2647
|
140
142
|
port_ocean/tests/core/utils/test_entity_topological_sorter.py,sha256=zuq5WSPy_88PemG3mOUIHTxWMR_js1R7tOzUYlgBd68,3447
|
141
143
|
port_ocean/tests/core/utils/test_resolve_entities_diff.py,sha256=4kTey1c0dWKbLXjJ-9m2GXrHyAWZeLQ2asdtYRRUdRs,16573
|
@@ -160,8 +162,8 @@ port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,32
|
|
160
162
|
port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
|
161
163
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
162
164
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
163
|
-
port_ocean-0.18.
|
164
|
-
port_ocean-0.18.
|
165
|
-
port_ocean-0.18.
|
166
|
-
port_ocean-0.18.
|
167
|
-
port_ocean-0.18.
|
165
|
+
port_ocean-0.18.5.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
166
|
+
port_ocean-0.18.5.dist-info/METADATA,sha256=qIcD5yXlV1899BUdBrFNoPzqda6yrqyrQFG27dZoOoc,6669
|
167
|
+
port_ocean-0.18.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
168
|
+
port_ocean-0.18.5.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
169
|
+
port_ocean-0.18.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|