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.
@@ -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,,