port-ocean 0.18.4__py3-none-any.whl → 0.18.5__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.18.4
3
+ Version: 0.18.5
4
4
  Summary: Port Ocean is a CLI tool for managing your Port projects.
5
5
  Home-page: https://app.getport.io
6
6
  Keywords: ocean,port-ocean,port
@@ -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=tf254jMqBW1GBmYDhfXMCkOqHA7C_chaYp1OY3Dfnsg,5869
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=6VbKPwFzsWG0IYsVD81hxSmfqtHUFqrfUuj1DBX5g4w,853
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=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
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.4.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
164
- port_ocean-0.18.4.dist-info/METADATA,sha256=qe_3HL2F4NgoCId8n4gmLPqVUGKaOTU11XC05vMPO8I,6669
165
- port_ocean-0.18.4.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
166
- port_ocean-0.18.4.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
167
- port_ocean-0.18.4.dist-info/RECORD,,
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,,