port-ocean 0.30.2__py3-none-any.whl → 0.30.3__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/helpers/async_client.py +29 -0
- port_ocean/helpers/stream.py +4 -1
- port_ocean/tests/clients/test_streaming_wrapper.py +85 -0
- {port_ocean-0.30.2.dist-info → port_ocean-0.30.3.dist-info}/METADATA +2 -1
- {port_ocean-0.30.2.dist-info → port_ocean-0.30.3.dist-info}/RECORD +8 -7
- {port_ocean-0.30.2.dist-info → port_ocean-0.30.3.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.30.2.dist-info → port_ocean-0.30.3.dist-info}/WHEEL +0 -0
- {port_ocean-0.30.2.dist-info → port_ocean-0.30.3.dist-info}/entry_points.txt +0 -0
|
@@ -5,6 +5,7 @@ from loguru import logger
|
|
|
5
5
|
|
|
6
6
|
from port_ocean.helpers.retry import RetryTransport, RetryConfig
|
|
7
7
|
from port_ocean.helpers.stream import Stream
|
|
8
|
+
from typing import AsyncGenerator, List, Dict
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class OceanAsyncClient(httpx.AsyncClient):
|
|
@@ -56,3 +57,31 @@ class OceanAsyncClient(httpx.AsyncClient):
|
|
|
56
57
|
response = await self.send(req, stream=True)
|
|
57
58
|
response.raise_for_status()
|
|
58
59
|
return Stream(response)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class StreamingClientWrapper:
|
|
63
|
+
def __init__(self, http_client: OceanAsyncClient):
|
|
64
|
+
self._client = http_client
|
|
65
|
+
|
|
66
|
+
async def stream_json(
|
|
67
|
+
self,
|
|
68
|
+
url: str,
|
|
69
|
+
target_items_path: str,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> AsyncGenerator[List[Dict[str, Any]], None]:
|
|
72
|
+
"""
|
|
73
|
+
A wrapper that provides a unified async generator interface for both streaming
|
|
74
|
+
and non-streaming HTTP GET requests.
|
|
75
|
+
|
|
76
|
+
:param url: The URL to request.
|
|
77
|
+
:param target_items_path: A JMESPath string to extract the list of items
|
|
78
|
+
from the JSON response (e.g., 'results'). The wrapper
|
|
79
|
+
will automatically adapt this for the streaming parser.
|
|
80
|
+
:param kwargs: Additional arguments for the HTTP request.
|
|
81
|
+
"""
|
|
82
|
+
# ijson needs a path to the items inside the array, e.g., "results.item"
|
|
83
|
+
streaming_path = f"{target_items_path}.item"
|
|
84
|
+
stream_response = await self._client.get_stream(url, **kwargs)
|
|
85
|
+
json_stream = stream_response.get_json_stream(target_items=streaming_path)
|
|
86
|
+
async for items_batch in json_stream:
|
|
87
|
+
yield items_batch
|
port_ocean/helpers/stream.py
CHANGED
|
@@ -22,7 +22,10 @@ class Stream:
|
|
|
22
22
|
if chunk_size is None:
|
|
23
23
|
chunk_size = ocean_context.ocean.config.streaming.chunk_size
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
streaming_location = ocean_context.ocean.config.streaming.location
|
|
26
|
+
os.makedirs(streaming_location, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
file_name = f"{streaming_location}/{uuid.uuid4()}"
|
|
26
29
|
|
|
27
30
|
crypt = Fernet(Fernet.generate_key())
|
|
28
31
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock
|
|
3
|
+
from typing import Any, AsyncGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from port_ocean.helpers.async_client import OceanAsyncClient, StreamingClientWrapper
|
|
7
|
+
from port_ocean.helpers.stream import Stream
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MockStream:
|
|
11
|
+
"""A mock of the port_ocean.helpers.stream.Stream class for testing."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, data_chunks: list[list[dict[str, Any]]]) -> None:
|
|
14
|
+
self._data_chunks = data_chunks
|
|
15
|
+
|
|
16
|
+
async def get_json_stream(self, target_items: str) -> AsyncGenerator[Any, None]:
|
|
17
|
+
"""An async generator that yields the data chunks."""
|
|
18
|
+
for chunk in self._data_chunks:
|
|
19
|
+
yield chunk
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_stream_json_with_streaming_enabled() -> None:
|
|
24
|
+
"""
|
|
25
|
+
Tests that when streaming is enabled, the wrapper uses the `get_stream` method
|
|
26
|
+
and yields data in batches.
|
|
27
|
+
"""
|
|
28
|
+
# Arrange
|
|
29
|
+
mock_client = AsyncMock(spec=OceanAsyncClient)
|
|
30
|
+
# Simulate receiving the data in two separate chunks/batches
|
|
31
|
+
mock_stream_instance = MockStream(
|
|
32
|
+
[[{"id": 1, "name": "one"}], [{"id": 2, "name": "two"}]]
|
|
33
|
+
)
|
|
34
|
+
mock_client.get_stream.return_value = mock_stream_instance
|
|
35
|
+
|
|
36
|
+
wrapper = StreamingClientWrapper(http_client=mock_client)
|
|
37
|
+
|
|
38
|
+
# Act
|
|
39
|
+
results = [item async for item in wrapper.stream_json("http://test.com", "results")]
|
|
40
|
+
|
|
41
|
+
# Assert
|
|
42
|
+
mock_client.get_stream.assert_called_once_with("http://test.com")
|
|
43
|
+
mock_client.get.assert_not_called()
|
|
44
|
+
assert results == [[{"id": 1, "name": "one"}], [{"id": 2, "name": "two"}]]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_stream_json_with_empty_results_streaming() -> None:
|
|
49
|
+
"""
|
|
50
|
+
Tests that the wrapper handles an empty stream correctly in streaming mode.
|
|
51
|
+
"""
|
|
52
|
+
# Arrange
|
|
53
|
+
mock_client = AsyncMock(spec=OceanAsyncClient)
|
|
54
|
+
mock_stream_instance = MockStream([[]]) # Stream yields one empty batch
|
|
55
|
+
mock_client.get_stream.return_value = mock_stream_instance
|
|
56
|
+
|
|
57
|
+
wrapper = StreamingClientWrapper(http_client=mock_client)
|
|
58
|
+
|
|
59
|
+
# Act
|
|
60
|
+
results = [item async for item in wrapper.stream_json("http://test.com", "results")]
|
|
61
|
+
|
|
62
|
+
# Assert
|
|
63
|
+
assert results == [[]]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_stream_json_path_adaptation_for_streaming() -> None:
|
|
68
|
+
"""
|
|
69
|
+
Tests that the wrapper correctly adapts the `target_items_path` for the
|
|
70
|
+
streaming parser (ijson) by appending `.item`.
|
|
71
|
+
"""
|
|
72
|
+
# Arrange
|
|
73
|
+
mock_client = AsyncMock(spec=OceanAsyncClient)
|
|
74
|
+
mock_stream = AsyncMock(spec=Stream)
|
|
75
|
+
mock_client.get_stream.return_value = mock_stream
|
|
76
|
+
|
|
77
|
+
wrapper = StreamingClientWrapper(http_client=mock_client)
|
|
78
|
+
|
|
79
|
+
# Act
|
|
80
|
+
# We only need to trigger the call to check the arguments
|
|
81
|
+
_ = [item async for item in wrapper.stream_json("http://test.com", "results")]
|
|
82
|
+
|
|
83
|
+
# Assert
|
|
84
|
+
# Verify that get_json_stream was called with the modified path
|
|
85
|
+
mock_stream.get_json_stream.assert_called_once_with(target_items="results.item")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: port-ocean
|
|
3
|
-
Version: 0.30.
|
|
3
|
+
Version: 0.30.3
|
|
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
|
|
@@ -33,6 +33,7 @@ Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
|
33
33
|
Requires-Dist: ijson (>=3.4.0,<4.0.0)
|
|
34
34
|
Requires-Dist: jinja2 (>=3.1.6)
|
|
35
35
|
Requires-Dist: jinja2-time (>=0.2.0,<0.3.0) ; extra == "cli"
|
|
36
|
+
Requires-Dist: jmespath (>=1.0.1,<2.0.0)
|
|
36
37
|
Requires-Dist: jq (>=1.8.0,<2.0.0)
|
|
37
38
|
Requires-Dist: loguru (>=0.7.0,<0.8.0)
|
|
38
39
|
Requires-Dist: prometheus-client (>=0.21.1,<0.22.0)
|
|
@@ -147,11 +147,11 @@ port_ocean/exceptions/port_defaults.py,sha256=2a7Koy541KxMan33mU-gbauUxsumG3NT4i
|
|
|
147
147
|
port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnqI,197
|
|
148
148
|
port_ocean/exceptions/webhook_processor.py,sha256=4SnkVzVwiacH_Ip4qs1hRHa6GanhnojW_TLTdQQtm7Y,363
|
|
149
149
|
port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
150
|
-
port_ocean/helpers/async_client.py,sha256=
|
|
150
|
+
port_ocean/helpers/async_client.py,sha256=GDRNq9kqb-7o6pAxSNZvE7zGh5semTr0oHoADuNjP-E,3325
|
|
151
151
|
port_ocean/helpers/metric/metric.py,sha256=9pxpDT-nP0nTZ2kIutCTFuF7IwOQcSJ3dkBA1CVSurc,14626
|
|
152
152
|
port_ocean/helpers/metric/utils.py,sha256=1lAgrxnZLuR_wUNDyPOPzLrm32b8cDdioob2lvnPQ1A,1619
|
|
153
153
|
port_ocean/helpers/retry.py,sha256=UiOUo89hUrY0VVLL8sMR8GAYg-UVf2Y3yJ-8hBU1I7E,20285
|
|
154
|
-
port_ocean/helpers/stream.py,sha256=
|
|
154
|
+
port_ocean/helpers/stream.py,sha256=i984XbY0hGWTuv7oe4k1byUkCd79D-HVBH3ijxsDgmM,2442
|
|
155
155
|
port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
156
156
|
port_ocean/log/handlers.py,sha256=LJ1WAfq7wYCrBpeLPihMKmWjdSahKKXNHFMRYkbk0Co,3630
|
|
157
157
|
port_ocean/log/logger_setup.py,sha256=wcr5WOkYRtng4pW6ZRl4Av3GqtZ2omSWIqYhB_8Duuc,2700
|
|
@@ -171,6 +171,7 @@ port_ocean/tests/clients/oauth/test_oauth_client.py,sha256=2XVMQUalDpiD539Z7_dk5
|
|
|
171
171
|
port_ocean/tests/clients/port/mixins/test_entities.py,sha256=jOMJ3ICUlhjjPTo4q6qUrEjTKvXRLUE6KjqjdFiDRBY,10766
|
|
172
172
|
port_ocean/tests/clients/port/mixins/test_integrations.py,sha256=vRt_EMsLozQC1LJNXxlvnHj3-FlOBGgAYxg5T0IAqtA,7621
|
|
173
173
|
port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=zzKYz3h8dl4Z5A2QG_924m0y9U6XTth1XYOfwNrd_24,914
|
|
174
|
+
port_ocean/tests/clients/test_streaming_wrapper.py,sha256=uTc0uaNynG7UgbRKNFaHURh8GYdG1O_cbS89veCxk-U,2902
|
|
174
175
|
port_ocean/tests/config/test_config.py,sha256=Rk4N-ldVSOfn1p23NzdVdfqUpPrqG2cMut4Sv-sAOrw,1023
|
|
175
176
|
port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
|
|
176
177
|
port_ocean/tests/core/conftest.py,sha256=0Oql7R1iTbjPyNdUoO6M21IKknLwnCIgDRz2JQ7nf0w,7748
|
|
@@ -220,8 +221,8 @@ port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,32
|
|
|
220
221
|
port_ocean/utils/signal.py,sha256=J1sI-e_32VHP_VUa5bskLMFoJjJOAk5isrnewKDikUI,2125
|
|
221
222
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
|
222
223
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
|
223
|
-
port_ocean-0.30.
|
|
224
|
-
port_ocean-0.30.
|
|
225
|
-
port_ocean-0.30.
|
|
226
|
-
port_ocean-0.30.
|
|
227
|
-
port_ocean-0.30.
|
|
224
|
+
port_ocean-0.30.3.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
225
|
+
port_ocean-0.30.3.dist-info/METADATA,sha256=fgABnumshGTta5I9-Dm9mu1WHQa4uPmZrSx6E3VZ7do,7095
|
|
226
|
+
port_ocean-0.30.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
227
|
+
port_ocean-0.30.3.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
|
228
|
+
port_ocean-0.30.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|