port-ocean 0.18.5__py3-none-any.whl → 0.18.7__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|