port-ocean 0.18.5__py3-none-any.whl → 0.18.7__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/ocean.py +32 -0
- port_ocean/core/handlers/entities_state_applier/port/applier.py +18 -2
- port_ocean/core/handlers/port_app_config/models.py +3 -0
- port_ocean/core/handlers/queue/__init__.py +4 -0
- port_ocean/core/handlers/queue/abstract_queue.py +28 -0
- port_ocean/core/handlers/queue/local_queue.py +25 -0
- port_ocean/core/handlers/webhook/__init__.py +0 -0
- port_ocean/core/handlers/webhook/abstract_webhook_processor.py +101 -0
- port_ocean/core/handlers/webhook/processor_manager.py +237 -0
- port_ocean/core/handlers/webhook/webhook_event.py +77 -0
- port_ocean/core/integrations/mixins/sync.py +2 -1
- port_ocean/core/integrations/mixins/sync_raw.py +26 -20
- port_ocean/exceptions/webhook_processor.py +4 -0
- port_ocean/ocean.py +7 -1
- port_ocean/tests/core/handlers/entities_state_applier/test_applier.py +86 -0
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +149 -124
- port_ocean/tests/core/handlers/queue/test_local_queue.py +90 -0
- port_ocean/tests/core/handlers/webhook/test_abstract_webhook_processor.py +114 -0
- port_ocean/tests/core/handlers/webhook/test_processor_manager.py +391 -0
- port_ocean/tests/core/handlers/webhook/test_webhook_event.py +65 -0
- port_ocean/utils/signal.py +6 -2
- {port_ocean-0.18.5.dist-info → port_ocean-0.18.7.dist-info}/METADATA +1 -1
- {port_ocean-0.18.5.dist-info → port_ocean-0.18.7.dist-info}/RECORD +26 -13
- {port_ocean-0.18.5.dist-info → port_ocean-0.18.7.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.18.5.dist-info → port_ocean-0.18.7.dist-info}/WHEEL +0 -0
- {port_ocean-0.18.5.dist-info → port_ocean-0.18.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,391 @@
|
|
1
|
+
import asyncio
|
2
|
+
import pytest
|
3
|
+
from fastapi import APIRouter
|
4
|
+
from typing import Dict, Any
|
5
|
+
|
6
|
+
from port_ocean.exceptions.webhook_processor import RetryableError
|
7
|
+
from port_ocean.core.handlers.webhook.processor_manager import WebhookProcessorManager
|
8
|
+
from port_ocean.core.handlers.webhook.abstract_webhook_processor import (
|
9
|
+
AbstractWebhookProcessor,
|
10
|
+
)
|
11
|
+
from port_ocean.core.handlers.webhook.webhook_event import (
|
12
|
+
WebhookEvent,
|
13
|
+
)
|
14
|
+
from port_ocean.core.handlers.queue import LocalQueue
|
15
|
+
from port_ocean.utils.signal import SignalHandler
|
16
|
+
|
17
|
+
|
18
|
+
class MockWebhookProcessor(AbstractWebhookProcessor):
|
19
|
+
def __init__(self, event: WebhookEvent) -> None:
|
20
|
+
super().__init__(event)
|
21
|
+
self.processed = False
|
22
|
+
self.cancel_called = False
|
23
|
+
self.error_to_raise: Exception | asyncio.CancelledError | None = None
|
24
|
+
self.retry_count = 0
|
25
|
+
self.max_retries = 3
|
26
|
+
|
27
|
+
async def authenticate(
|
28
|
+
self, payload: Dict[str, Any], headers: Dict[str, str]
|
29
|
+
) -> bool:
|
30
|
+
return True
|
31
|
+
|
32
|
+
async def validate_payload(self, payload: Dict[str, Any]) -> bool:
|
33
|
+
return True
|
34
|
+
|
35
|
+
async def handle_event(self, payload: Dict[str, Any]) -> None:
|
36
|
+
if self.error_to_raise:
|
37
|
+
raise self.error_to_raise
|
38
|
+
self.processed = True
|
39
|
+
|
40
|
+
async def cancel(self) -> None:
|
41
|
+
self.cancel_called = True
|
42
|
+
|
43
|
+
|
44
|
+
class RetryableProcessor(MockWebhookProcessor):
|
45
|
+
def __init__(self, event: WebhookEvent) -> None:
|
46
|
+
super().__init__(event)
|
47
|
+
self.attempt_count = 0
|
48
|
+
|
49
|
+
async def handle_event(self, payload: Dict[str, Any]) -> None:
|
50
|
+
self.attempt_count += 1
|
51
|
+
if self.attempt_count < 3: # Succeed on third attempt
|
52
|
+
raise RetryableError("Temporary failure")
|
53
|
+
self.processed = True
|
54
|
+
|
55
|
+
|
56
|
+
class TestableWebhookProcessorManager(WebhookProcessorManager):
|
57
|
+
__test__ = False
|
58
|
+
|
59
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
60
|
+
super().__init__(*args, **kwargs)
|
61
|
+
self.running_processors: list[AbstractWebhookProcessor] = []
|
62
|
+
self.no_matching_processors: bool = False
|
63
|
+
|
64
|
+
def _extract_matching_processors(
|
65
|
+
self, event: WebhookEvent, path: str
|
66
|
+
) -> list[AbstractWebhookProcessor]:
|
67
|
+
try:
|
68
|
+
return super()._extract_matching_processors(event, path)
|
69
|
+
except ValueError:
|
70
|
+
self.no_matching_processors = True
|
71
|
+
raise
|
72
|
+
|
73
|
+
async def _process_single_event(
|
74
|
+
self, processor: AbstractWebhookProcessor, path: str
|
75
|
+
) -> None:
|
76
|
+
self.running_processors.append(processor)
|
77
|
+
await super()._process_single_event(processor, path)
|
78
|
+
|
79
|
+
|
80
|
+
class TestWebhookProcessorManager:
|
81
|
+
@pytest.fixture
|
82
|
+
def router(self) -> APIRouter:
|
83
|
+
return APIRouter()
|
84
|
+
|
85
|
+
@pytest.fixture
|
86
|
+
def signal_handler(self) -> SignalHandler:
|
87
|
+
return SignalHandler()
|
88
|
+
|
89
|
+
@pytest.fixture
|
90
|
+
def processor_manager(
|
91
|
+
self, router: APIRouter, signal_handler: SignalHandler
|
92
|
+
) -> TestableWebhookProcessorManager:
|
93
|
+
return TestableWebhookProcessorManager(router, signal_handler)
|
94
|
+
|
95
|
+
@pytest.fixture
|
96
|
+
def mock_event(self) -> WebhookEvent:
|
97
|
+
return WebhookEvent.from_dict(
|
98
|
+
{
|
99
|
+
"payload": {"test": "data"},
|
100
|
+
"headers": {"content-type": "application/json"},
|
101
|
+
"trace_id": "test-trace",
|
102
|
+
}
|
103
|
+
)
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def assert_event_processed_successfully(
|
107
|
+
processor: MockWebhookProcessor,
|
108
|
+
) -> None:
|
109
|
+
"""Assert that a processor's event was processed successfully"""
|
110
|
+
assert processor.processed, "Event was not processed successfully"
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def assert_event_processed_with_error(processor: MockWebhookProcessor) -> None:
|
114
|
+
"""Assert that an event was processed with an error"""
|
115
|
+
assert not processor.processed, "Event did not fail as expected"
|
116
|
+
|
117
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
118
|
+
async def test_register_handler(
|
119
|
+
self, processor_manager: TestableWebhookProcessorManager
|
120
|
+
) -> None:
|
121
|
+
"""Test registering a processor for a path."""
|
122
|
+
processor_manager.register_processor("/test", MockWebhookProcessor)
|
123
|
+
assert "/test" in processor_manager._processors
|
124
|
+
assert len(processor_manager._processors["/test"]) == 1
|
125
|
+
assert isinstance(processor_manager._event_queues["/test"], LocalQueue)
|
126
|
+
|
127
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
128
|
+
async def test_register_multiple_handlers_with_filters(
|
129
|
+
self, processor_manager: TestableWebhookProcessorManager
|
130
|
+
) -> None:
|
131
|
+
"""Test registering multiple processors with different filters."""
|
132
|
+
|
133
|
+
def filter1(e: WebhookEvent) -> bool:
|
134
|
+
return e.payload.get("type") == "type1"
|
135
|
+
|
136
|
+
def filter2(e: WebhookEvent) -> bool:
|
137
|
+
return e.payload.get("type") == "type2"
|
138
|
+
|
139
|
+
processor_manager.register_processor("/test", MockWebhookProcessor, filter1)
|
140
|
+
processor_manager.register_processor("/test", MockWebhookProcessor, filter2)
|
141
|
+
|
142
|
+
assert len(processor_manager._processors["/test"]) == 2
|
143
|
+
|
144
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
145
|
+
async def test_successful_event_processing(
|
146
|
+
self,
|
147
|
+
processor_manager: TestableWebhookProcessorManager,
|
148
|
+
mock_event: WebhookEvent,
|
149
|
+
) -> None:
|
150
|
+
"""Test successful processing of an event."""
|
151
|
+
processed_events: list[MockWebhookProcessor] = []
|
152
|
+
|
153
|
+
class SuccessProcessor(MockWebhookProcessor):
|
154
|
+
async def handle_event(self, payload: Dict[str, Any]) -> None:
|
155
|
+
self.processed = True
|
156
|
+
processed_events.append(self)
|
157
|
+
|
158
|
+
processor_manager.register_processor("/test", SuccessProcessor)
|
159
|
+
|
160
|
+
await processor_manager.start_processing_event_messages()
|
161
|
+
await processor_manager._event_queues["/test"].put(mock_event)
|
162
|
+
|
163
|
+
# Allow time for processing
|
164
|
+
await asyncio.sleep(0.1)
|
165
|
+
|
166
|
+
# Verify at least one processor ran and completed successfully
|
167
|
+
assert len(processed_events) > 0
|
168
|
+
for processor in processed_events:
|
169
|
+
self.assert_event_processed_successfully(processor)
|
170
|
+
|
171
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
172
|
+
async def test_graceful_shutdown(
|
173
|
+
self,
|
174
|
+
processor_manager: TestableWebhookProcessorManager,
|
175
|
+
mock_event: WebhookEvent,
|
176
|
+
) -> None:
|
177
|
+
"""Test graceful shutdown with in-flight requests"""
|
178
|
+
processor_manager.register_processor("/test", MockWebhookProcessor)
|
179
|
+
|
180
|
+
await processor_manager.start_processing_event_messages()
|
181
|
+
await processor_manager._event_queues["/test"].put(mock_event)
|
182
|
+
|
183
|
+
# Start shutdown
|
184
|
+
await processor_manager.shutdown()
|
185
|
+
|
186
|
+
# Verify all tasks are cleaned up
|
187
|
+
assert len(processor_manager._webhook_processor_tasks) == 0
|
188
|
+
self.assert_event_processed_successfully(
|
189
|
+
processor_manager.running_processors[0] # type: ignore
|
190
|
+
)
|
191
|
+
|
192
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
193
|
+
async def test_handler_filter_matching(
|
194
|
+
self, processor_manager: TestableWebhookProcessorManager
|
195
|
+
) -> None:
|
196
|
+
"""Test that processors are selected based on their filters."""
|
197
|
+
type1_event = WebhookEvent.from_dict(
|
198
|
+
{"payload": {"type": "type1"}, "headers": {}, "trace_id": "test-trace-1"}
|
199
|
+
)
|
200
|
+
|
201
|
+
type2_event = WebhookEvent.from_dict(
|
202
|
+
{"payload": {"type": "type2"}, "headers": {}, "trace_id": "test-trace-2"}
|
203
|
+
)
|
204
|
+
|
205
|
+
def filter1(e: WebhookEvent) -> bool:
|
206
|
+
return e.payload.get("type") == "type1"
|
207
|
+
|
208
|
+
def filter2(e: WebhookEvent) -> bool:
|
209
|
+
return e.payload.get("type") == "type2"
|
210
|
+
|
211
|
+
processor_manager.register_processor("/test", MockWebhookProcessor, filter1)
|
212
|
+
processor_manager.register_processor("/test", MockWebhookProcessor, filter2)
|
213
|
+
|
214
|
+
await processor_manager.start_processing_event_messages()
|
215
|
+
|
216
|
+
# Process both events
|
217
|
+
await processor_manager._event_queues["/test"].put(type1_event)
|
218
|
+
await processor_manager._event_queues["/test"].put(type2_event)
|
219
|
+
|
220
|
+
await asyncio.sleep(0.1)
|
221
|
+
|
222
|
+
# Verify both events were processed
|
223
|
+
self.assert_event_processed_successfully(
|
224
|
+
processor_manager.running_processors[0] # type: ignore
|
225
|
+
)
|
226
|
+
self.assert_event_processed_successfully(
|
227
|
+
processor_manager.running_processors[1] # type: ignore
|
228
|
+
)
|
229
|
+
|
230
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
231
|
+
async def test_handler_timeout(
|
232
|
+
self, router: APIRouter, signal_handler: SignalHandler, mock_event: WebhookEvent
|
233
|
+
) -> None:
|
234
|
+
"""Test processor timeout behavior."""
|
235
|
+
|
236
|
+
# Set a short timeout for testing
|
237
|
+
processor_manager = TestableWebhookProcessorManager(
|
238
|
+
router, signal_handler, max_event_processing_seconds=0.1
|
239
|
+
)
|
240
|
+
|
241
|
+
class TimeoutHandler(MockWebhookProcessor):
|
242
|
+
async def handle_event(self, payload: Dict[str, Any]) -> None:
|
243
|
+
await asyncio.sleep(2) # Longer than max_handler_processing_seconds
|
244
|
+
|
245
|
+
processor_manager.register_processor("/test", TimeoutHandler)
|
246
|
+
await processor_manager.start_processing_event_messages()
|
247
|
+
await processor_manager._event_queues["/test"].put(mock_event)
|
248
|
+
|
249
|
+
# Wait long enough for the timeout to occur
|
250
|
+
await asyncio.sleep(0.2)
|
251
|
+
|
252
|
+
self.assert_event_processed_with_error(
|
253
|
+
processor_manager.running_processors[0] # type: ignore
|
254
|
+
)
|
255
|
+
|
256
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
257
|
+
async def test_handler_cancellation(
|
258
|
+
self,
|
259
|
+
processor_manager: TestableWebhookProcessorManager,
|
260
|
+
mock_event: WebhookEvent,
|
261
|
+
) -> None:
|
262
|
+
"""Test processor cancellation during shutdown."""
|
263
|
+
cancelled_events: list[WebhookEvent] = []
|
264
|
+
|
265
|
+
class CanceledHandler(MockWebhookProcessor):
|
266
|
+
async def handle_event(self, payload: Dict[str, Any]) -> None:
|
267
|
+
await asyncio.sleep(0.2)
|
268
|
+
|
269
|
+
async def cancel(self) -> None:
|
270
|
+
cancelled_events.append(self.event)
|
271
|
+
self.event.payload["canceled"] = True
|
272
|
+
|
273
|
+
processor_manager.register_processor("/test", CanceledHandler)
|
274
|
+
await processor_manager.start_processing_event_messages()
|
275
|
+
await processor_manager._event_queues["/test"].put(mock_event)
|
276
|
+
|
277
|
+
await asyncio.sleep(0.1)
|
278
|
+
|
279
|
+
# Wait for the event to be processed
|
280
|
+
await processor_manager._cancel_all_tasks()
|
281
|
+
|
282
|
+
# Verify at least one event was cancelled
|
283
|
+
assert len(cancelled_events) > 0
|
284
|
+
assert any(event.payload.get("canceled") for event in cancelled_events)
|
285
|
+
|
286
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
287
|
+
async def test_invalid_handler_registration(self) -> None:
|
288
|
+
"""Test registration of invalid processor type."""
|
289
|
+
handler_manager = WebhookProcessorManager(APIRouter(), SignalHandler())
|
290
|
+
|
291
|
+
with pytest.raises(ValueError):
|
292
|
+
handler_manager.register_processor("/test", object) # type: ignore
|
293
|
+
|
294
|
+
async def test_no_matching_handlers(
|
295
|
+
self,
|
296
|
+
processor_manager: TestableWebhookProcessorManager,
|
297
|
+
mock_event: WebhookEvent,
|
298
|
+
) -> None:
|
299
|
+
"""Test behavior when no processors match the event."""
|
300
|
+
processor_manager.register_processor(
|
301
|
+
"/test", MockWebhookProcessor, lambda e: False
|
302
|
+
)
|
303
|
+
|
304
|
+
await processor_manager.start_processing_event_messages()
|
305
|
+
await processor_manager._event_queues["/test"].put(mock_event)
|
306
|
+
|
307
|
+
await asyncio.sleep(0.1)
|
308
|
+
|
309
|
+
assert processor_manager.no_matching_processors
|
310
|
+
assert len(processor_manager.running_processors) == 0
|
311
|
+
|
312
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
313
|
+
async def test_multiple_processors(
|
314
|
+
self, processor_manager: TestableWebhookProcessorManager
|
315
|
+
) -> None:
|
316
|
+
# Test multiple processors for same path
|
317
|
+
processor_manager.register_processor("/test", MockWebhookProcessor)
|
318
|
+
processor_manager.register_processor("/test", MockWebhookProcessor)
|
319
|
+
assert len(processor_manager._processors["/test"]) == 2
|
320
|
+
|
321
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
322
|
+
async def test_all_matching_processors_execute(
|
323
|
+
self,
|
324
|
+
processor_manager: TestableWebhookProcessorManager,
|
325
|
+
mock_event: WebhookEvent,
|
326
|
+
) -> None:
|
327
|
+
"""Test that all matching processors are executed even if some fail."""
|
328
|
+
processed_count = 0
|
329
|
+
|
330
|
+
class SuccessProcessor(MockWebhookProcessor):
|
331
|
+
async def handle_event(self, payload: Dict[str, Any]) -> None:
|
332
|
+
nonlocal processed_count
|
333
|
+
processed_count += 1
|
334
|
+
self.processed = True
|
335
|
+
|
336
|
+
class FailingProcessor(MockWebhookProcessor):
|
337
|
+
async def handle_event(self, payload: Dict[str, Any]) -> None:
|
338
|
+
raise Exception("Simulated failure")
|
339
|
+
|
340
|
+
# Register mix of successful and failing processors
|
341
|
+
processor_manager.register_processor("/test", SuccessProcessor)
|
342
|
+
processor_manager.register_processor("/test", FailingProcessor)
|
343
|
+
processor_manager.register_processor("/test", SuccessProcessor)
|
344
|
+
|
345
|
+
await processor_manager.start_processing_event_messages()
|
346
|
+
await processor_manager._event_queues["/test"].put(mock_event)
|
347
|
+
|
348
|
+
# Wait for processing to complete
|
349
|
+
await asyncio.sleep(0.1)
|
350
|
+
|
351
|
+
# Verify successful processors ran despite failing one
|
352
|
+
assert processed_count == 2
|
353
|
+
|
354
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
355
|
+
async def test_retry_mechanism(
|
356
|
+
self,
|
357
|
+
processor_manager: TestableWebhookProcessorManager,
|
358
|
+
mock_event: WebhookEvent,
|
359
|
+
) -> None:
|
360
|
+
"""Test retry mechanism with temporary failures."""
|
361
|
+
processor = MockWebhookProcessor(mock_event)
|
362
|
+
processor.error_to_raise = RetryableError("Temporary failure")
|
363
|
+
|
364
|
+
# Simulate 2 failures before success
|
365
|
+
async def handle_event(payload: Dict[str, Any]) -> None:
|
366
|
+
if processor.retry_count < 2:
|
367
|
+
raise RetryableError("Temporary failure")
|
368
|
+
processor.processed = True
|
369
|
+
|
370
|
+
processor.handle_event = handle_event # type: ignore
|
371
|
+
|
372
|
+
await processor_manager._process_webhook_request(processor)
|
373
|
+
|
374
|
+
assert processor.processed
|
375
|
+
assert processor.retry_count == 2
|
376
|
+
|
377
|
+
@pytest.mark.skip(reason="Temporarily ignoring this test")
|
378
|
+
async def test_max_retries_exceeded(
|
379
|
+
self,
|
380
|
+
processor_manager: TestableWebhookProcessorManager,
|
381
|
+
mock_event: WebhookEvent,
|
382
|
+
) -> None:
|
383
|
+
"""Test behavior when max retries are exceeded."""
|
384
|
+
processor = MockWebhookProcessor(mock_event)
|
385
|
+
processor.max_retries = 1
|
386
|
+
processor.error_to_raise = RetryableError("Temporary failure")
|
387
|
+
|
388
|
+
with pytest.raises(RetryableError):
|
389
|
+
await processor_manager._process_webhook_request(processor)
|
390
|
+
|
391
|
+
assert processor.retry_count == processor.max_retries
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import pytest
|
2
|
+
from fastapi import Request
|
3
|
+
from port_ocean.core.handlers.webhook.webhook_event import (
|
4
|
+
EventHeaders,
|
5
|
+
EventPayload,
|
6
|
+
WebhookEvent,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
class TestWebhookEvent:
|
11
|
+
@pytest.fixture
|
12
|
+
def sample_payload(self) -> EventPayload:
|
13
|
+
return {"test": "data", "nested": {"value": 123}}
|
14
|
+
|
15
|
+
@pytest.fixture
|
16
|
+
def sample_headers(self) -> EventHeaders:
|
17
|
+
return {"content-type": "application/json", "x-test-header": "test-value"}
|
18
|
+
|
19
|
+
@pytest.fixture
|
20
|
+
def mock_request(
|
21
|
+
self, sample_payload: EventPayload, sample_headers: EventHeaders
|
22
|
+
) -> Request:
|
23
|
+
scope = {
|
24
|
+
"type": "http",
|
25
|
+
"headers": [(k.encode(), v.encode()) for k, v in sample_headers.items()],
|
26
|
+
}
|
27
|
+
mock_request = Request(scope)
|
28
|
+
mock_request._json = sample_payload
|
29
|
+
return mock_request
|
30
|
+
|
31
|
+
@pytest.fixture
|
32
|
+
def webhook_event(
|
33
|
+
self, sample_payload: EventPayload, sample_headers: EventHeaders
|
34
|
+
) -> WebhookEvent:
|
35
|
+
return WebhookEvent(
|
36
|
+
trace_id="test-trace-id",
|
37
|
+
payload=sample_payload,
|
38
|
+
headers=sample_headers,
|
39
|
+
)
|
40
|
+
|
41
|
+
async def test_create_from_request(self, mock_request: Request) -> None:
|
42
|
+
"""Test creating WebhookEvent from a request."""
|
43
|
+
event = await WebhookEvent.from_request(mock_request)
|
44
|
+
|
45
|
+
assert event.trace_id is not None
|
46
|
+
assert len(event.trace_id) > 0
|
47
|
+
assert event.headers == dict(mock_request.headers)
|
48
|
+
assert event._original_request == mock_request
|
49
|
+
|
50
|
+
def test_create_from_dict(
|
51
|
+
self, sample_payload: EventPayload, sample_headers: EventHeaders
|
52
|
+
) -> None:
|
53
|
+
"""Test creating WebhookEvent from a dictionary."""
|
54
|
+
data = {
|
55
|
+
"trace_id": "test-trace-id",
|
56
|
+
"payload": sample_payload,
|
57
|
+
"headers": sample_headers,
|
58
|
+
}
|
59
|
+
|
60
|
+
event = WebhookEvent.from_dict(data)
|
61
|
+
|
62
|
+
assert event.trace_id == "test-trace-id"
|
63
|
+
assert event.payload == sample_payload
|
64
|
+
assert event.headers == sample_headers
|
65
|
+
assert event._original_request is None
|
port_ocean/utils/signal.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
from asyncio import iscoroutinefunction
|
1
2
|
from typing import Callable, Any
|
2
3
|
|
3
4
|
from werkzeug.local import LocalProxy, LocalStack
|
@@ -13,13 +14,16 @@ class SignalHandler:
|
|
13
14
|
def __init__(self) -> None:
|
14
15
|
self._handlers: dict[str, Callable[[], Any]] = {}
|
15
16
|
|
16
|
-
def exit(self) -> None:
|
17
|
+
async def exit(self) -> None:
|
17
18
|
"""
|
18
19
|
Handles the exit signal.
|
19
20
|
"""
|
20
21
|
while self._handlers:
|
21
22
|
_, handler = self._handlers.popitem()
|
22
|
-
handler
|
23
|
+
if iscoroutinefunction(handler):
|
24
|
+
await handler()
|
25
|
+
else:
|
26
|
+
handler()
|
23
27
|
|
24
28
|
def register(self, callback: Callable[[], Any]) -> str:
|
25
29
|
_id = generate_uuid()
|
@@ -64,7 +64,7 @@ port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
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
66
|
port_ocean/context/event.py,sha256=QK2ben4fJtxdorq_yRroATttP0DRc4wLtlUJ1as5D58,6208
|
67
|
-
port_ocean/context/ocean.py,sha256
|
67
|
+
port_ocean/context/ocean.py,sha256=-T8Z_xcUNMeobr33TTABiOQjxC4KCVvVrP0IytRfen8,6425
|
68
68
|
port_ocean/context/resource.py,sha256=yDj63URzQelj8zJPh4BAzTtPhpKr9Gw9DRn7I_0mJ1s,1692
|
69
69
|
port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
70
70
|
port_ocean/core/defaults/__init__.py,sha256=8qCZg8n06WAdMu9s_FiRtDYLGPGHbOuS60vapeUoAks,142
|
@@ -84,7 +84,7 @@ port_ocean/core/handlers/base.py,sha256=cTarblazu8yh8xz2FpB-dzDKuXxtoi143XJgPbV_
|
|
84
84
|
port_ocean/core/handlers/entities_state_applier/__init__.py,sha256=kgLZDCeCEzi4r-0nzW9k78haOZNf6PX7mJOUr34A4c8,173
|
85
85
|
port_ocean/core/handlers/entities_state_applier/base.py,sha256=5wHL0icfFAYRPqk8iV_wN49GdJ3aRUtO8tumSxBi4Wo,2268
|
86
86
|
port_ocean/core/handlers/entities_state_applier/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
87
|
-
port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=
|
87
|
+
port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=6YketOyFDYrqK9tGM7FWZLO0bTXiYz0lQB-u3dyJvWw,6271
|
88
88
|
port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py,sha256=1zncwCbE-Gej0xaWKlzZgoXxOBe9bgs_YxlZ8QW3NdI,1751
|
89
89
|
port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=lyv6xKzhYfd6TioUgR3AVRSJqj7JpAaj1LxxU2xAqeo,1720
|
90
90
|
port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybem9MthOs7p1Wawac87uSXz9U8,156
|
@@ -93,16 +93,23 @@ port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=C6zJbS3m
|
|
93
93
|
port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iEgEeXtn2pRMrT4Dze5r1Ixbk,134
|
94
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
|
-
port_ocean/core/handlers/port_app_config/models.py,sha256=
|
96
|
+
port_ocean/core/handlers/port_app_config/models.py,sha256=5Tt2BWHOJ4nnkh4xVrwyVG4KO5Tp4e64Cu_eFF-9jBg,2449
|
97
|
+
port_ocean/core/handlers/queue/__init__.py,sha256=1fICM0ZLATmmj6f7cdq_eV2kmw0_jy7y2INuLQIpzIE,121
|
98
|
+
port_ocean/core/handlers/queue/abstract_queue.py,sha256=q_gpaWFFZHxM3XovEbgsDn8jEOLM45iAZWVC81Paxto,620
|
99
|
+
port_ocean/core/handlers/queue/local_queue.py,sha256=EzqsGIX43xbVAcePwTcCg5QDrXATQpy-VzWxxN_OyAM,574
|
97
100
|
port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
|
98
101
|
port_ocean/core/handlers/resync_state_updater/updater.py,sha256=Yg9ET6ZV5B9GW7u6zZA6GlB_71kmvxvYX2FWgQNzMvo,3182
|
102
|
+
port_ocean/core/handlers/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
103
|
+
port_ocean/core/handlers/webhook/abstract_webhook_processor.py,sha256=oZvYgE3rqirx7yN0PAL-9Nls1EHLwOHQaNtFzla3Heo,3500
|
104
|
+
port_ocean/core/handlers/webhook/processor_manager.py,sha256=B1NjvuWD5MXaW0PWxxOAkbkNfgC9oeuQreusna0EUBA,9166
|
105
|
+
port_ocean/core/handlers/webhook/webhook_event.py,sha256=9Lk4JzHfgWNruts3pYe-t6OgaG1xP4EkD6lFr_78yUk,2210
|
99
106
|
port_ocean/core/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
100
107
|
port_ocean/core/integrations/base.py,sha256=eS0WDOfCTim1UOQQrNuP14I6hvT_fr8dof_cr1ls01s,3107
|
101
108
|
port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4_RgZnwSRmTCreKcBVM,220
|
102
109
|
port_ocean/core/integrations/mixins/events.py,sha256=0jKRsBw6lU8Mqs7MaQK4n-t_H6Z4NEkXZ5VWzqTrKEc,2396
|
103
110
|
port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
|
104
|
-
port_ocean/core/integrations/mixins/sync.py,sha256=
|
105
|
-
port_ocean/core/integrations/mixins/sync_raw.py,sha256=
|
111
|
+
port_ocean/core/integrations/mixins/sync.py,sha256=GHiFbnw0XrBfl7aCTH_w67f_N7EZbcUgssc-0fPujNU,4047
|
112
|
+
port_ocean/core/integrations/mixins/sync_raw.py,sha256=7kk2p5lLKq9oivqqintZumuaIHSbcSmoUfWrE346l7g,24821
|
106
113
|
port_ocean/core/integrations/mixins/utils.py,sha256=oN4Okz6xlaefpid1_Pud8HPSw9BwwjRohyNsknq-Myg,2309
|
107
114
|
port_ocean/core/models.py,sha256=FvTp-BlpbvLbMbngE0wsiimsCfmIhUR1PvsE__Z--1I,2206
|
108
115
|
port_ocean/core/ocean_types.py,sha256=j_-or1VxDy22whLLxwxgzIsE4wAhFLH19Xff9l4oJA8,1124
|
@@ -117,6 +124,7 @@ port_ocean/exceptions/context.py,sha256=mA8HII6Rl4QxKUz98ppy1zX3kaziaen21h1ZWuU3
|
|
117
124
|
port_ocean/exceptions/core.py,sha256=Zmb1m6NnkSPWpAiQA5tgejm3zpDMt1WQEN47OJNo54A,856
|
118
125
|
port_ocean/exceptions/port_defaults.py,sha256=2a7Koy541KxMan33mU-gbauUxsumG3NT4itVxSpQqfw,666
|
119
126
|
port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnqI,197
|
127
|
+
port_ocean/exceptions/webhook_processor.py,sha256=yQYazg53Y-ohb7HfViwq1opH_ZUuUdhHSRxcUNveFpI,114
|
120
128
|
port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
121
129
|
port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861vE3K40,1783
|
122
130
|
port_ocean/helpers/retry.py,sha256=fmvaUFLIW6PICgYH4RI5rrKBXDxCAhR1Gtl0dsb2kMs,15625
|
@@ -125,7 +133,7 @@ port_ocean/log/handlers.py,sha256=ncVjgqrZRh6BhyRrA6DQG86Wsbxph1yWYuEC0cWfe-Q,36
|
|
125
133
|
port_ocean/log/logger_setup.py,sha256=CoEDowe5OwNOL_5clU6Z4faktfh0VWaOTS0VLmyhHjw,2404
|
126
134
|
port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2920
|
127
135
|
port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
|
128
|
-
port_ocean/ocean.py,sha256=
|
136
|
+
port_ocean/ocean.py,sha256=MmLGxx0EC3Q6FEpbnZc9_5A7DculsvN2nAz3aZkj2kI,5536
|
129
137
|
port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
130
138
|
port_ocean/run.py,sha256=COoRSmLG4hbsjIW5DzhV0NYVegI9xHd1POv6sg4U1No,2217
|
131
139
|
port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
|
@@ -134,10 +142,15 @@ port_ocean/tests/clients/port/mixins/test_entities.py,sha256=A9myrnkLhKSQrnOLv1Z
|
|
134
142
|
port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=-8iHM33Oe8PuyEfj3O_6Yob8POp4fSmB0hnIT0Gv-8Y,868
|
135
143
|
port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
|
136
144
|
port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
|
145
|
+
port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=R9bqyJocUWTh0NW0s-5ttD_SYYeM5EbYILgVmgWa7qA,2776
|
137
146
|
port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=FnEnaDjuoAbKvKyv6xJ46n3j0ZcaT70Sg2zc7oy7HAA,13596
|
138
|
-
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=
|
147
|
+
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=gxQ4e9hQuMS8-o5UbiUSt1I1uaK0DCO3yCFDVigpZvo,31740
|
139
148
|
port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
|
140
149
|
port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=s3D98JP3YV9V6T5PCDPE2852gkqGiDmo03UyexESX_I,6872
|
150
|
+
port_ocean/tests/core/handlers/queue/test_local_queue.py,sha256=9Ly0HzZXbs6Rbl_bstsIdInC3h2bgABU3roP9S_PnJM,2582
|
151
|
+
port_ocean/tests/core/handlers/webhook/test_abstract_webhook_processor.py,sha256=pMETxEW98FCpPs28VGKTtueRwA5ka4Z3--4CJpfNi2U,3911
|
152
|
+
port_ocean/tests/core/handlers/webhook/test_processor_manager.py,sha256=k7ZjegY4qSfkf5uRqdSpc_v8RhObE_20wd1AwrO08Q4,14976
|
153
|
+
port_ocean/tests/core/handlers/webhook/test_webhook_event.py,sha256=yfsXuJzwEe9WYzyhWwT2IOEwwD9dHtKp_ufREM5v_DI,2090
|
141
154
|
port_ocean/tests/core/test_utils.py,sha256=Z3kdhb5V7Svhcyy3EansdTpgHL36TL6erNtU-OPwAcI,2647
|
142
155
|
port_ocean/tests/core/utils/test_entity_topological_sorter.py,sha256=zuq5WSPy_88PemG3mOUIHTxWMR_js1R7tOzUYlgBd68,3447
|
143
156
|
port_ocean/tests/core/utils/test_resolve_entities_diff.py,sha256=4kTey1c0dWKbLXjJ-9m2GXrHyAWZeLQ2asdtYRRUdRs,16573
|
@@ -159,11 +172,11 @@ port_ocean/utils/cache.py,sha256=RgfN4SjjHrEkbqUChyboeD1mrXomolUUjsJtvbkmr3U,335
|
|
159
172
|
port_ocean/utils/misc.py,sha256=0q2cJ5psqxn_5u_56pT7vOVQ3shDM02iC1lzyWQ_zl0,2098
|
160
173
|
port_ocean/utils/queue_utils.py,sha256=KWWl8YVnG-glcfIHhM6nefY-2sou_C6DVP1VynQwzB4,2762
|
161
174
|
port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,3231
|
162
|
-
port_ocean/utils/signal.py,sha256=
|
175
|
+
port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
|
163
176
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
164
177
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
165
|
-
port_ocean-0.18.
|
166
|
-
port_ocean-0.18.
|
167
|
-
port_ocean-0.18.
|
168
|
-
port_ocean-0.18.
|
169
|
-
port_ocean-0.18.
|
178
|
+
port_ocean-0.18.7.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
179
|
+
port_ocean-0.18.7.dist-info/METADATA,sha256=ZWM3fXr54v89kiXWYwF3y6PHl28sjSUuf6BRV2PVAyI,6669
|
180
|
+
port_ocean-0.18.7.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
181
|
+
port_ocean-0.18.7.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
182
|
+
port_ocean-0.18.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|