port-ocean 0.27.3__py3-none-any.whl → 0.27.6__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.
@@ -28,11 +28,14 @@ ARG INTEGRATION_VERSION
28
28
  ARG BUILD_CONTEXT
29
29
  ARG PROMETHEUS_MULTIPROC_DIR=/tmp/ocean/prometheus/metrics
30
30
  ARG OAUTH_CONFIG_DIR=/app/.config
31
+ ARG STREAMING_LOCATION=/tmp/ocean/streaming
31
32
 
32
33
  ENV LIBRDKAFKA_VERSION=2.8.2 \
33
- PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR}
34
+ PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR} \
35
+ STREAMING_LOCATION=${STREAMING_LOCATION}
34
36
 
35
37
  RUN mkdir -p ${PROMETHEUS_MULTIPROC_DIR}
38
+ RUN mkdir -p ${STREAMING_LOCATION}
36
39
  RUN chown -R ocean:appgroup /tmp/ocean && chmod -R 755 /tmp/ocean
37
40
 
38
41
  RUN mkdir -p ${OAUTH_CONFIG_DIR}
@@ -33,13 +33,15 @@ RUN apt-get update \
33
33
 
34
34
  ARG BUILD_CONTEXT
35
35
  ARG PROMETHEUS_MULTIPROC_DIR=/tmp/ocean/prometheus/metrics
36
+ ARG STREAMING_LOCATION=/tmp/ocean/streaming
36
37
 
37
38
  ENV PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR}
38
-
39
+ ENV STREAMING_LOCATION=${STREAMING_LOCATION}
39
40
  # Create /tmp/ocean directory and set permissions
40
41
 
41
42
 
42
43
  RUN mkdir -p ${PROMETHEUS_MULTIPROC_DIR}
44
+ RUN mkdir -p ${STREAMING_LOCATION}
43
45
 
44
46
  WORKDIR /app
45
47
 
@@ -73,6 +73,13 @@ class MetricsSettings(BaseOceanModel, extra=Extra.allow):
73
73
  webhook_url: str | None = Field(default=None)
74
74
 
75
75
 
76
+ class StreamingSettings(BaseOceanModel, extra=Extra.allow):
77
+ enabled: bool = Field(default=False)
78
+ max_buffer_size_mb: int = Field(default=1024 * 1024 * 20) # 20 mb
79
+ chunk_size: int = Field(default=1024 * 64) # 64 kb
80
+ location: str = Field(default="/tmp/ocean/streaming")
81
+
82
+
76
83
  class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
77
84
  _integration_config_model: BaseModel | None = None
78
85
 
@@ -114,6 +121,8 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
114
121
  yield_items_to_parse: bool = False
115
122
  yield_items_to_parse_batch_size: int = 10
116
123
 
124
+ streaming: StreamingSettings = Field(default_factory=lambda: StreamingSettings())
125
+
117
126
  @validator("process_execution_mode")
118
127
  def validate_process_execution_mode(
119
128
  cls, process_execution_mode: ProcessExecutionMode
@@ -16,6 +16,7 @@ from port_ocean.core.event_listener.base import (
16
16
  EventListenerEvents,
17
17
  EventListenerSettings,
18
18
  )
19
+ from pydantic import validator
19
20
 
20
21
 
21
22
  class KafkaEventListenerSettings(EventListenerSettings):
@@ -46,6 +47,19 @@ class KafkaEventListenerSettings(EventListenerSettings):
46
47
  kafka_security_enabled: bool = True
47
48
  consumer_poll_timeout: int = 1
48
49
 
50
+ @validator("brokers")
51
+ @classmethod
52
+ def parse_brokers(cls, v: str) -> str:
53
+ # If it's a JSON array string, parse and join
54
+ if v.strip().startswith("[") and v.strip().endswith("]"):
55
+ try:
56
+ parsed = json.loads(v)
57
+ if isinstance(parsed, list):
58
+ return ",".join(parsed)
59
+ except json.JSONDecodeError:
60
+ pass
61
+ return v
62
+
49
63
  def get_changelog_destination_details(self) -> dict[str, Any]:
50
64
  """
51
65
  Returns the changelog destination configuration for the Kafka event listener.
@@ -4,6 +4,7 @@ import httpx
4
4
  from loguru import logger
5
5
 
6
6
  from port_ocean.helpers.retry import RetryTransport
7
+ from port_ocean.helpers.stream import Stream
7
8
 
8
9
 
9
10
  class OceanAsyncClient(httpx.AsyncClient):
@@ -50,3 +51,9 @@ class OceanAsyncClient(httpx.AsyncClient):
50
51
  logger=logger,
51
52
  **(self._transport_kwargs or {}),
52
53
  )
54
+
55
+ async def get_stream(self, url: str, **kwargs: Any) -> Stream:
56
+ req = self.build_request("GET", url, **kwargs)
57
+ response = await self.send(req, stream=True)
58
+ response.raise_for_status()
59
+ return Stream(response)
@@ -0,0 +1,71 @@
1
+ import os
2
+ from typing import Any, AsyncGenerator
3
+ import uuid
4
+
5
+ import aiofiles
6
+ import httpx
7
+ import ijson # type: ignore[import-untyped]
8
+ from cryptography.fernet import Fernet
9
+
10
+ import port_ocean.context.ocean as ocean_context
11
+
12
+
13
+ class Stream:
14
+ def __init__(self, response: httpx.Response):
15
+ self.response = response
16
+ self.headers = response.headers
17
+ self.status_code = response.status_code
18
+
19
+ async def _byte_stream(
20
+ self, chunk_size: int | None = None
21
+ ) -> AsyncGenerator[bytes, None]:
22
+ if chunk_size is None:
23
+ chunk_size = ocean_context.ocean.config.streaming.chunk_size
24
+
25
+ file_name = f"{ocean_context.ocean.config.streaming.location}/{uuid.uuid4()}"
26
+
27
+ crypt = Fernet(Fernet.generate_key())
28
+
29
+ try:
30
+ async for chunk in self.response.aiter_bytes(chunk_size=chunk_size):
31
+ async with aiofiles.open(f"{file_name}", "ab") as f:
32
+ if len(chunk) > 0:
33
+ await f.write(crypt.encrypt(chunk))
34
+ await f.write(b"\n")
35
+ finally:
36
+ await self.response.aclose()
37
+
38
+ try:
39
+ async with aiofiles.open(f"{file_name}", mode="rb") as f:
40
+ while True:
41
+ line = await f.readline()
42
+ if not line:
43
+ break
44
+ data = crypt.decrypt(line)
45
+ yield data
46
+ finally:
47
+ try:
48
+ os.remove(file_name)
49
+ except FileNotFoundError:
50
+ pass
51
+
52
+ async def get_json_stream(
53
+ self,
54
+ target_items: str = "",
55
+ max_buffer_size_mb: int | None = None,
56
+ ) -> AsyncGenerator[list[dict[str, Any]], None]:
57
+ if max_buffer_size_mb is None:
58
+ max_buffer_size_mb = ocean_context.ocean.config.streaming.max_buffer_size_mb
59
+
60
+ events = ijson.sendable_list()
61
+ coro = ijson.items_coro(events, target_items)
62
+ current_buffer_size = 0
63
+ async for chunk in self._byte_stream():
64
+ coro.send(chunk)
65
+ current_buffer_size += len(chunk)
66
+ if current_buffer_size >= max_buffer_size_mb:
67
+ if len(events) > 0:
68
+ yield events
69
+ events.clear()
70
+ current_buffer_size = 0
71
+ yield events
@@ -0,0 +1,70 @@
1
+ from port_ocean.core.event_listener.kafka import KafkaEventListenerSettings
2
+ import pytest
3
+ from pydantic import ValidationError
4
+
5
+
6
+ def test_default_kafka_settings() -> None:
7
+ """Test default values are properly set"""
8
+ config = KafkaEventListenerSettings(type="KAFKA")
9
+ assert config.type == "KAFKA"
10
+ assert config.security_protocol == "SASL_SSL"
11
+ assert config.authentication_mechanism == "SCRAM-SHA-512"
12
+ assert config.kafka_security_enabled is True
13
+ assert config.consumer_poll_timeout == 1
14
+ assert "b-1-public.publicclusterprod" in config.brokers
15
+
16
+
17
+ def test_brokers_json_array_parsing() -> None:
18
+ """Test that JSON array strings get converted to comma-separated"""
19
+ json_brokers = '["broker1:9092", "broker2:9092", "broker3:9092"]'
20
+ config = KafkaEventListenerSettings(type="KAFKA", brokers=json_brokers)
21
+ assert config.brokers == "broker1:9092,broker2:9092,broker3:9092"
22
+
23
+
24
+ def test_brokers_regular_string_unchanged() -> None:
25
+ """Test that regular comma-separated strings pass through unchanged"""
26
+ regular_brokers = "broker1:9092,broker2:9092"
27
+ config = KafkaEventListenerSettings(type="KAFKA", brokers=regular_brokers)
28
+ assert config.brokers == regular_brokers
29
+
30
+
31
+ def test_brokers_malformed_json_unchanged() -> None:
32
+ """Test that malformed JSON strings don't break validation"""
33
+ bad_json = "[broker1:9092, broker2:9092"
34
+ config = KafkaEventListenerSettings(type="KAFKA", brokers=bad_json)
35
+ assert config.brokers == bad_json
36
+
37
+
38
+ def test_custom_values() -> None:
39
+ """Test overriding default values"""
40
+ config = KafkaEventListenerSettings(
41
+ type="KAFKA",
42
+ brokers="custom:9092",
43
+ security_protocol="PLAINTEXT",
44
+ authentication_mechanism="PLAIN",
45
+ kafka_security_enabled=False,
46
+ consumer_poll_timeout=5,
47
+ )
48
+ assert config.brokers == "custom:9092"
49
+ assert config.security_protocol == "PLAINTEXT"
50
+ assert config.authentication_mechanism == "PLAIN"
51
+ assert config.kafka_security_enabled is False
52
+ assert config.consumer_poll_timeout == 5
53
+
54
+
55
+ def test_type_literal_validation() -> None:
56
+ """Test that type field only accepts KAFKA"""
57
+ with pytest.raises(ValidationError):
58
+ KafkaEventListenerSettings(type="RABBITMQ") # type: ignore[arg-type]
59
+
60
+
61
+ def test_empty_brokers_array() -> None:
62
+ """Test empty JSON array becomes empty string"""
63
+ config = KafkaEventListenerSettings(type="KAFKA", brokers="[]")
64
+ assert config.brokers == ""
65
+
66
+
67
+ def test_single_broker_array() -> None:
68
+ """Test single broker in JSON array"""
69
+ config = KafkaEventListenerSettings(type="KAFKA", brokers='["single:9092"]')
70
+ assert config.brokers == "single:9092"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.27.3
3
+ Version: 0.27.6
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
@@ -22,12 +22,15 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Classifier: Topic :: Utilities
24
24
  Provides-Extra: cli
25
+ Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
25
26
  Requires-Dist: aiostream (>=0.5.2,<0.7.0)
26
27
  Requires-Dist: click (>=8.1.3,<9.0.0) ; extra == "cli"
27
28
  Requires-Dist: confluent-kafka (>=2.10.1,<3.0.0)
28
29
  Requires-Dist: cookiecutter (>=2.1.1,<3.0.0) ; extra == "cli"
29
- Requires-Dist: fastapi (>=0.115.3,<0.116.0)
30
+ Requires-Dist: cryptography (>=44.0.1,<45.0.0)
31
+ Requires-Dist: fastapi (>=0.116.0,<0.117.0)
30
32
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
33
+ Requires-Dist: ijson (>=3.4.0,<4.0.0)
31
34
  Requires-Dist: jinja2 (>=3.1.6)
32
35
  Requires-Dist: jinja2-time (>=0.2.0,<0.3.0) ; extra == "cli"
33
36
  Requires-Dist: jq (>=1.8.0,<2.0.0)
@@ -1,9 +1,9 @@
1
- integrations/_infra/Dockerfile.Deb,sha256=ovmwNBRNrblTW6K9ru2FNTiT9qDgI7_zY28O__VMW6I,2367
1
+ integrations/_infra/Dockerfile.Deb,sha256=weK3VrskDEsuvYaxerak-XIqDgTx_wPorAFQQjYH_FU,2493
2
2
  integrations/_infra/Dockerfile.alpine,sha256=7E4Sb-8supsCcseerHwTkuzjHZoYcaHIyxiBZ-wewo0,3482
3
3
  integrations/_infra/Dockerfile.base.builder,sha256=ESe1PKC6itp_AuXawbLI75k1Kruny6NTANaTinxOgVs,743
4
4
  integrations/_infra/Dockerfile.base.runner,sha256=uAcs2IsxrAAUHGXt_qULA5INr-HFguf5a5fCKiqEzbY,384
5
5
  integrations/_infra/Dockerfile.dockerignore,sha256=CM1Fxt3I2AvSvObuUZRmy5BNLSGC7ylnbpWzFgD4cso,1163
6
- integrations/_infra/Dockerfile.local,sha256=v-K0jgDj5vWyF4HCL8-wluUPB04pf9ycEjusRBmIVnc,1527
6
+ integrations/_infra/Dockerfile.local,sha256=FFX9RvFqlaHvhUrRnnzUl0zQp2oKDFVRGkXJQPMQ7cI,1650
7
7
  integrations/_infra/Makefile,sha256=YgLKvuF_Dw4IA7X98Nus6zIW_3cJ60M1QFGs3imj5c4,2430
8
8
  integrations/_infra/README.md,sha256=ZtJFSMCTU5zTeM8ddRuW1ZL1ga8z7Ic2F3mxmgOSjgo,1195
9
9
  integrations/_infra/entry_local.sh,sha256=Sn2TexTEpruH2ixIAGsk-fZV6Y7pT3jd2Pi9TxBeFuw,633
@@ -70,7 +70,7 @@ port_ocean/clients/port/utils.py,sha256=osFyAjw7Y5Qf2uVSqC7_RTCQfijiL1zS74JJM0go
70
70
  port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
71
  port_ocean/config/base.py,sha256=x1gFbzujrxn7EJudRT81C6eN9WsYAb3vOHwcpcpX8Tc,6370
72
72
  port_ocean/config/dynamic.py,sha256=Lrk4JRGtR-0YKQ9DDGexX5NGFE7EJ6VoHya19YYhssM,2687
73
- port_ocean/config/settings.py,sha256=-jgVUBiFbeVjd9GjkMH6cRLLQHhtvI9x1WNdVE5s_p0,7311
73
+ port_ocean/config/settings.py,sha256=Zz_D40EXZEm0hzNdYgwdUy_s5LbJ6iMg3Zcl2n5NLUY,7686
74
74
  port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
76
76
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -87,7 +87,7 @@ port_ocean/core/event_listener/__init__.py,sha256=T3E52MKs79fNEW381p7zU9F2vOMvIi
87
87
  port_ocean/core/event_listener/base.py,sha256=GFBTHiYhCzps50phzopQFUlTGAluQkCRlyaRqOG4g1Y,2995
88
88
  port_ocean/core/event_listener/factory.py,sha256=M4Qi05pI840sjDIbdjUEgYe9Gp5ckoCkX-KgLBxUpZg,4096
89
89
  port_ocean/core/event_listener/http.py,sha256=_hkQmi9nNh8YG6hbfLrhkATsmGVO8y3qBWvrBHX5Nhk,2992
90
- port_ocean/core/event_listener/kafka.py,sha256=pVQfGtElIJN2P5lPBrMxyl05B8c2Q2wp5WvA6Pqjnyc,7501
90
+ port_ocean/core/event_listener/kafka.py,sha256=7bUq4EoVv5sIBxiJnfhi7zdAI7whKoSIaS64Ic9B4Ys,7963
91
91
  port_ocean/core/event_listener/once.py,sha256=6DhxQLSz5hjEGhmgLU7aA2ZVRQw3tuwh8sh7N63gRdU,5855
92
92
  port_ocean/core/event_listener/polling.py,sha256=iZt59z4Dyus_wVvNfIPbq-G1CozAW9kfPU7uRDLPJVE,3604
93
93
  port_ocean/core/event_listener/webhooks_only.py,sha256=PTWnmbLtbJb3ySfotMpTWMYgDVy9zOSYIIGqNbWK0UU,1214
@@ -140,10 +140,11 @@ port_ocean/exceptions/port_defaults.py,sha256=2a7Koy541KxMan33mU-gbauUxsumG3NT4i
140
140
  port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnqI,197
141
141
  port_ocean/exceptions/webhook_processor.py,sha256=4SnkVzVwiacH_Ip4qs1hRHa6GanhnojW_TLTdQQtm7Y,363
142
142
  port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
- port_ocean/helpers/async_client.py,sha256=0MkFY46MuEYkju7gJ8HYeUTMBsjwAzESvATryJy2DaM,1698
143
+ port_ocean/helpers/async_client.py,sha256=LOgUlZ5Cs_WUSc8XujCVjPGvzZ_3AuFJNKPy0FKV3fA,1987
144
144
  port_ocean/helpers/metric/metric.py,sha256=Aacz7bOd8ZCwEPpXAdwLbKRXf28Z4wiViG_GXiV_xWg,14529
145
145
  port_ocean/helpers/metric/utils.py,sha256=1lAgrxnZLuR_wUNDyPOPzLrm32b8cDdioob2lvnPQ1A,1619
146
146
  port_ocean/helpers/retry.py,sha256=VHAp6j9-Vid6aNR5sca3S0aW6b1S2oYw9vT9hi1N22U,18556
147
+ port_ocean/helpers/stream.py,sha256=_UwsThzXynxWzL8OlBT1pmb2evZBi9HaaqeAGNuTuOI,2338
147
148
  port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
149
  port_ocean/log/handlers.py,sha256=ncVjgqrZRh6BhyRrA6DQG86Wsbxph1yWYuEC0cWfe-Q,3631
149
150
  port_ocean/log/logger_setup.py,sha256=0K3zVG0YYrYOWEV8-rCGks1o-bMRxgHXlqawu9w_tSw,2656
@@ -167,6 +168,7 @@ port_ocean/tests/config/test_config.py,sha256=Rk4N-ldVSOfn1p23NzdVdfqUpPrqG2cMut
167
168
  port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
168
169
  port_ocean/tests/core/conftest.py,sha256=5shShx81LRuQTBwS2sDIjNJO2LSD6cUNz46SgNYzjGY,7686
169
170
  port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
171
+ port_ocean/tests/core/event_listener/test_kafka.py,sha256=PH90qk2fvdrQOSZD2QrvkGy8w_WoYb_KHGnqJ6PLHAo,2681
170
172
  port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=7XWgwUB9uVYRov4VbIz1A-7n2YLbHTTYT-4rKJxjB0A,10711
171
173
  port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=TjSj8ssIqH23VJlO5PGovbudCqDbuE2-54iNQsD9K-I,14099
172
174
  port_ocean/tests/core/handlers/mixins/test_live_events.py,sha256=6yUsYooBYchiZP_eYa8PN1IhiztJShBdPguoseyNMzY,12482
@@ -205,8 +207,8 @@ port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,32
205
207
  port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
206
208
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
207
209
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
208
- port_ocean-0.27.3.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
209
- port_ocean-0.27.3.dist-info/METADATA,sha256=8q1fasdb2UPwIRETpBdDwk5m9IEDUOrBEBl5fSruMZ8,6887
210
- port_ocean-0.27.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
211
- port_ocean-0.27.3.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
212
- port_ocean-0.27.3.dist-info/RECORD,,
210
+ port_ocean-0.27.6.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
211
+ port_ocean-0.27.6.dist-info/METADATA,sha256=KzFgTmio2jk9gaqUyvyZDhMIsyanGS28gXG0J-1gzdg,7015
212
+ port_ocean-0.27.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
213
+ port_ocean-0.27.6.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
214
+ port_ocean-0.27.6.dist-info/RECORD,,