port-ocean 0.12.2.dev22__py3-none-any.whl → 0.12.4__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.

Potentially problematic release.


This version of port-ocean might be problematic. Click here for more details.

@@ -1,23 +1,13 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- import click
4
3
  import json
5
- from cookiecutter.main import cookiecutter # type: ignore
6
4
  import os
7
5
 
8
- from port_ocean.cli.commands.main import cli_start, print_logo, console
9
- from port_ocean.cli.utils import cli_root_path
10
-
11
-
12
- def symlink_makefile(result: str, name: str) -> None:
13
- infra_make_file = os.path.join(
14
- os.path.dirname(result), "../integrations/_infra/Makefile"
15
- )
16
- target_link_make_file = os.path.join(
17
- os.path.dirname(result), f"../integrations/{name}/Makefile"
18
- )
6
+ import click
7
+ from cookiecutter.main import cookiecutter # type: ignore
19
8
 
20
- os.link(infra_make_file, target_link_make_file)
9
+ from port_ocean.cli.commands.main import cli_start, console, print_logo
10
+ from port_ocean.cli.utils import cli_root_path
21
11
 
22
12
 
23
13
  def add_vscode_configuration(result: str, name: str) -> None:
@@ -73,11 +63,13 @@ def new(path: str, is_private_integration: bool) -> None:
73
63
  "is_private_integration": is_private_integration,
74
64
  },
75
65
  )
66
+
76
67
  name = result.split("/")[-1]
77
68
 
78
- if not is_private_integration:
69
+ final_private_integration = os.path.exists(os.path.join(result, "Dockerfile"))
70
+
71
+ if not final_private_integration:
79
72
  add_vscode_configuration(result, name)
80
- symlink_makefile(result, name)
81
73
 
82
74
  console.print(
83
75
  "\n🌊 Ahoy, Captain! Your project is ready to set sail into the vast ocean of possibilities!",
@@ -95,7 +87,7 @@ def new(path: str, is_private_integration: bool) -> None:
95
87
  "⚓️ Set sail with [blue]Ocean[/blue]: Run [bold][blue]ocean sail[/blue] <path_to_integration>[/bold] to run the project using Ocean.\n"
96
88
  f"▶️ [bold][blue]ocean sail {path}/{name}[/blue][/bold] \n"
97
89
  )
98
- if not is_private_integration:
90
+ if not final_private_integration:
99
91
  console.print(
100
92
  "⚓️ Smooth sailing with [blue]Make[/blue]: Alternatively, you can run [bold][blue]make run[/blue][/bold] to launch your project using Make. \n"
101
93
  f"▶️ [bold][blue]make run {path}/{name}[/blue][/bold]"
@@ -1,13 +1,19 @@
1
1
  import os
2
+ import shutil
2
3
 
3
4
 
4
5
  def handle_private_integration_flags():
5
- print("{{ cookiecutter.is_private_integration }}")
6
+ infra_make_file = "../_infra/Makefile"
7
+ target_link_make_file = os.path.join("./Makefile")
8
+
6
9
  if "{{ cookiecutter.is_private_integration }}" == "True":
10
+ shutil.copyfile(infra_make_file, target_link_make_file)
7
11
  os.remove("sonar-project.properties")
8
- if "{{ cookiecutter.is_private_integration }}" == "False":
9
- os.remove("Dockerfile")
10
- os.remove(".dockerignore")
12
+ return
13
+
14
+ os.symlink(infra_make_file, target_link_make_file)
15
+ os.remove("Dockerfile")
16
+ os.remove(".dockerignore")
11
17
 
12
18
 
13
19
  if __name__ == "__main__":
@@ -1,7 +1,7 @@
1
1
  from typing import Callable, TYPE_CHECKING, Any, Literal, Union
2
2
 
3
+ from fastapi import APIRouter
3
4
  from pydantic.main import BaseModel
4
- from starlette.routing import Router
5
5
  from werkzeug.local import LocalProxy
6
6
 
7
7
  from port_ocean.clients.port.types import UserAgentType
@@ -45,7 +45,7 @@ class PortOceanContext:
45
45
  return self.app.config
46
46
 
47
47
  @property
48
- def router(self) -> Router:
48
+ def router(self) -> APIRouter:
49
49
  return self.app.integration_router
50
50
 
51
51
  @property
@@ -77,6 +77,5 @@ class BaseIntegration(SyncRawMixin, SyncMixin):
77
77
  )
78
78
 
79
79
  logger.info("Initializing event listener")
80
- # event_listener = await self.event_listener_factory.create_event_listener()
81
- # await event_listener.start()
82
-
80
+ event_listener = await self.event_listener_factory.create_event_listener()
81
+ await event_listener.start()
@@ -3,6 +3,7 @@ import inspect
3
3
  import typing
4
4
  from typing import Callable, Awaitable, Any
5
5
 
6
+ import httpx
6
7
  from loguru import logger
7
8
 
8
9
  from port_ocean.clients.port.types import UserAgentType
@@ -172,7 +173,6 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
172
173
  self, resource_config: ResourceConfig, user_agent_type: UserAgentType
173
174
  ) -> tuple[list[Entity], list[Exception]]:
174
175
  results, errors = await self._get_resource_raw_results(resource_config)
175
- passed_entities = []
176
176
  async_generators: list[ASYNC_GENERATOR_RESYNC_TYPE] = []
177
177
  raw_results: RAW_RESULT = []
178
178
  for result in results:
@@ -201,14 +201,14 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
201
201
  0, send_raw_data_examples_amount - len(passed_entities)
202
202
  )
203
203
 
204
- # entities, register_errors = await self._register_resource_raw(
205
- # resource_config,
206
- # items,
207
- # user_agent_type,
208
- # send_raw_data_examples_amount=send_raw_data_examples_amount,
209
- # )
210
- # errors.extend(register_errors)
211
- # passed_entities.extend(entities.passed)
204
+ entities, register_errors = await self._register_resource_raw(
205
+ resource_config,
206
+ items,
207
+ user_agent_type,
208
+ send_raw_data_examples_amount=send_raw_data_examples_amount,
209
+ )
210
+ errors.extend(register_errors)
211
+ passed_entities.extend(entities.passed)
212
212
  except* OceanAbortException as error:
213
213
  errors.append(error)
214
214
 
@@ -426,20 +426,20 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
426
426
  use_cache=False
427
427
  )
428
428
  logger.info(f"Resync will use the following mappings: {app_config.dict()}")
429
- # try:
430
- # did_fetched_current_state = True
431
- # entities_at_port = await ocean.port_client.search_entities(
432
- # user_agent_type
433
- # )
434
- # except httpx.HTTPError as e:
435
- # logger.warning(
436
- # "Failed to fetch the current state of entities at Port. "
437
- # "Skipping delete phase due to unknown initial state. "
438
- # f"Error: {e}\n"
439
- # f"Response status code: {e.response.status_code if isinstance(e, httpx.HTTPStatusError) else None}\n"
440
- # f"Response content: {e.response.text if isinstance(e, httpx.HTTPStatusError) else None}\n"
441
- # )
442
- # did_fetched_current_state = False
429
+ try:
430
+ did_fetched_current_state = True
431
+ entities_at_port = await ocean.port_client.search_entities(
432
+ user_agent_type
433
+ )
434
+ except httpx.HTTPError as e:
435
+ logger.warning(
436
+ "Failed to fetch the current state of entities at Port. "
437
+ "Skipping delete phase due to unknown initial state. "
438
+ f"Error: {e}\n"
439
+ f"Response status code: {e.response.status_code if isinstance(e, httpx.HTTPStatusError) else None}\n"
440
+ f"Response content: {e.response.text if isinstance(e, httpx.HTTPStatusError) else None}\n"
441
+ )
442
+ did_fetched_current_state = False
443
443
 
444
444
  creation_results: list[tuple[list[Entity], list[Exception]]] = []
445
445
 
@@ -458,36 +458,36 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
458
458
  except asyncio.CancelledError as e:
459
459
  logger.warning("Resync aborted successfully, skipping delete phase. This leads to an incomplete state")
460
460
  raise
461
- # else:
462
- # if not did_fetched_current_state:
463
- # logger.warning(
464
- # "Due to an error before the resync, the previous state of entities at Port is unknown."
465
- # " Skipping delete phase due to unknown initial state."
466
- # )
467
- # return
468
- #
469
- # logger.info("Starting resync diff calculation")
470
- # flat_created_entities, errors = zip_and_sum(creation_results) or [
471
- # [],
472
- # [],
473
- # ]
474
- #
475
- # if errors:
476
- # message = f"Resync failed with {len(errors)}. Skipping delete phase due to incomplete state"
477
- # error_group = ExceptionGroup(
478
- # f"Resync failed with {len(errors)}. Skipping delete phase due to incomplete state",
479
- # errors,
480
- # )
481
- # if not silent:
482
- # raise error_group
483
- #
484
- # logger.error(message, exc_info=error_group)
485
- # else:
486
- # logger.info(
487
- # f"Running resync diff calculation, number of entities at Port before resync: {len(entities_at_port)}, number of entities created during sync: {len(flat_created_entities)}"
488
- # )
489
- # await self.entities_state_applier.delete_diff(
490
- # {"before": entities_at_port, "after": flat_created_entities},
491
- # user_agent_type,
492
- # )
493
- # logger.info("Resync finished successfully")
461
+ else:
462
+ if not did_fetched_current_state:
463
+ logger.warning(
464
+ "Due to an error before the resync, the previous state of entities at Port is unknown."
465
+ " Skipping delete phase due to unknown initial state."
466
+ )
467
+ return
468
+
469
+ logger.info("Starting resync diff calculation")
470
+ flat_created_entities, errors = zip_and_sum(creation_results) or [
471
+ [],
472
+ [],
473
+ ]
474
+
475
+ if errors:
476
+ message = f"Resync failed with {len(errors)}. Skipping delete phase due to incomplete state"
477
+ error_group = ExceptionGroup(
478
+ f"Resync failed with {len(errors)}. Skipping delete phase due to incomplete state",
479
+ errors,
480
+ )
481
+ if not silent:
482
+ raise error_group
483
+
484
+ logger.error(message, exc_info=error_group)
485
+ else:
486
+ logger.info(
487
+ f"Running resync diff calculation, number of entities at Port before resync: {len(entities_at_port)}, number of entities created during sync: {len(flat_created_entities)}"
488
+ )
489
+ await self.entities_state_applier.delete_diff(
490
+ {"before": entities_at_port, "after": flat_created_entities},
491
+ user_agent_type,
492
+ )
493
+ logger.info("Resync finished successfully")
@@ -1,7 +1,7 @@
1
1
  import abc
2
2
 
3
+ from fastapi.responses import Response, PlainTextResponse
3
4
  from port_ocean.exceptions.base import BaseOceanException
4
- from starlette.responses import Response, PlainTextResponse
5
5
 
6
6
 
7
7
  class BaseAPIException(BaseOceanException, abc.ABC):
@@ -15,8 +15,8 @@ from port_ocean.utils.signal import signal_handler
15
15
  def setup_logger(level: LogLevelType, enable_http_handler: bool) -> None:
16
16
  logger.remove()
17
17
  _stdout_loguru_handler(level)
18
- # if enable_http_handler:
19
- # _http_loguru_handler(level)
18
+ if enable_http_handler:
19
+ _http_loguru_handler(level)
20
20
 
21
21
 
22
22
  def _stdout_loguru_handler(level: LogLevelType) -> None:
port_ocean/middlewares.py CHANGED
@@ -1,70 +1,73 @@
1
+ from typing import Callable, Awaitable
2
+
3
+ from fastapi import Request, Response
1
4
  from loguru import logger
2
- from starlette.requests import Request
3
- from starlette.responses import Response
4
5
 
5
6
  from port_ocean.exceptions.api import BaseAPIException, InternalServerException
6
7
  from .context.event import event_context, EventType
7
8
  from .context.ocean import ocean
8
9
  from .utils.misc import get_time, generate_uuid
9
- from starlette.middleware.base import (
10
- BaseHTTPMiddleware,
11
- RequestResponseEndpoint,
12
- )
13
10
 
14
11
 
15
- class RequestHandlerMiddleware(BaseHTTPMiddleware):
16
- async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
17
- start_time = get_time(seconds_precision=False)
18
- request_id = generate_uuid()
12
+ async def _handle_silently(
13
+ call_next: Callable[[Request], Awaitable[Response]], request: Request
14
+ ) -> Response:
15
+ response: Response
16
+ try:
17
+ if request.url.path.startswith("/integration"):
18
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request"):
19
+ await ocean.integration.port_app_config_handler.get_port_app_config()
20
+ response = await call_next(request)
21
+ else:
22
+ response = await call_next(request)
19
23
 
20
- with logger.contextualize(request_id=request_id):
21
- log_level = (
22
- "DEBUG"
23
- if request.url.path == "/docs" or request.url.path == "/openapi.json"
24
- else "INFO"
24
+ except BaseAPIException as ex:
25
+ response = ex.response()
26
+ if response.status_code < 500:
27
+ logger.bind(exception=str(ex)).info(
28
+ "Request did not succeed due to client-side error"
25
29
  )
26
- logger.bind(url=str(request.url), method=request.method).log(
27
- log_level, f"Request to {request.url.path} started"
30
+ else:
31
+ logger.opt(exception=True).warning(
32
+ "Request did not succeed due to server-side error"
28
33
  )
29
- response = await self._handle_silently(request, call_next)
30
34
 
31
- end_time = get_time(seconds_precision=False)
32
- time_elapsed = round(end_time - start_time, 5)
33
- response.headers["X-Request-ID"] = request_id
34
- response.headers["X-Process-Time"] = str(time_elapsed)
35
- logger.bind(
36
- time_elapsed=time_elapsed, response_status=response.status_code
37
- ).log(log_level, f"Request to {request.url.path} ended")
35
+ except Exception:
36
+ logger.opt(exception=True).error("Request failed due to unexpected error")
37
+ response = InternalServerException().response()
38
38
 
39
- return response
39
+ return response
40
40
 
41
- async def _handle_silently(
42
- self, request: Request, call_next: RequestResponseEndpoint
43
- ) -> Response:
44
- response: Response
45
- try:
46
- if request.url.path.startswith("/integration"):
47
- async with event_context(
48
- EventType.HTTP_REQUEST, trigger_type="request"
49
- ):
50
- await ocean.integration.port_app_config_handler.get_port_app_config()
51
- response = await call_next(request)
52
- else:
53
- response = await call_next(request)
54
41
 
55
- except BaseAPIException as ex:
56
- response = ex.response()
57
- if response.status_code < 500:
58
- logger.bind(exception=str(ex)).info(
59
- "Request did not succeed due to client-side error"
60
- )
61
- else:
62
- logger.opt(exception=True).warning(
63
- "Request did not succeed due to server-side error"
64
- )
42
+ async def request_handler(
43
+ request: Request, call_next: Callable[[Request], Awaitable[Response]]
44
+ ) -> Response:
45
+ """Middleware used by FastAPI to process each request, featuring:
46
+
47
+ - Contextualize request logs with a unique Request ID (UUID4) for each unique request.
48
+ - Catch exceptions during the request handling. Translate custom API exceptions into responses,
49
+ or treat (and log) unexpected exceptions.
50
+ """
51
+ start_time = get_time(seconds_precision=False)
52
+ request_id = generate_uuid()
53
+
54
+ with logger.contextualize(request_id=request_id):
55
+ log_level = (
56
+ "DEBUG"
57
+ if request.url.path == "/docs" or request.url.path == "/openapi.json"
58
+ else "INFO"
59
+ )
60
+ logger.bind(url=str(request.url), method=request.method).log(
61
+ log_level, f"Request to {request.url.path} started"
62
+ )
63
+ response = await _handle_silently(call_next, request)
65
64
 
66
- except Exception:
67
- logger.opt(exception=True).error("Request failed due to unexpected error")
68
- response = InternalServerException().response()
65
+ end_time = get_time(seconds_precision=False)
66
+ time_elapsed = round(end_time - start_time, 5)
67
+ response.headers["X-Request-ID"] = request_id
68
+ response.headers["X-Process-Time"] = str(time_elapsed)
69
+ logger.bind(
70
+ time_elapsed=time_elapsed, response_status=response.status_code
71
+ ).log(log_level, f"Request to {request.url.path} ended")
69
72
 
70
73
  return response
port_ocean/ocean.py CHANGED
@@ -1,18 +1,16 @@
1
1
  import asyncio
2
- import contextlib
3
2
  import sys
4
3
  import threading
4
+ from contextlib import asynccontextmanager
5
5
  from typing import Callable, Any, Dict, AsyncIterator, Type
6
6
 
7
7
  from fastapi import FastAPI, APIRouter
8
8
  from loguru import logger
9
9
  from pydantic import BaseModel
10
- from starlette.applications import Starlette
11
- from starlette.middleware import Middleware
12
- from starlette.responses import JSONResponse
13
- from starlette.routing import Route, Mount, Router
14
10
  from starlette.types import Scope, Receive, Send
15
11
 
12
+ from port_ocean.core.handlers.resync_state_updater import ResyncStateUpdater
13
+ from port_ocean.core.models import Runtime
16
14
  from port_ocean.clients.port.client import PortClient
17
15
  from port_ocean.config.settings import (
18
16
  IntegrationConfiguration,
@@ -22,29 +20,27 @@ from port_ocean.context.ocean import (
22
20
  ocean,
23
21
  initialize_port_ocean_context,
24
22
  )
25
- from port_ocean.core.handlers.resync_state_updater import ResyncStateUpdater
26
23
  from port_ocean.core.integrations.base import BaseIntegration
27
- from port_ocean.core.models import Runtime
28
24
  from port_ocean.log.sensetive import sensitive_log_filter
29
- from port_ocean.middlewares import RequestHandlerMiddleware
25
+ from port_ocean.middlewares import request_handler
30
26
  from port_ocean.utils.repeat import repeat_every
31
27
  from port_ocean.utils.signal import signal_handler
32
28
  from port_ocean.version import __integration_version__
29
+ from port_ocean.utils.misc import IntegrationStateStatus
33
30
 
34
31
 
35
32
  class Ocean:
36
33
  def __init__(
37
34
  self,
38
- app: Starlette | None = None,
35
+ app: FastAPI | None = None,
39
36
  integration_class: Callable[[PortOceanContext], BaseIntegration] | None = None,
40
- integration_router: Router | None = None,
37
+ integration_router: APIRouter | None = None,
41
38
  config_factory: Type[BaseModel] | None = None,
42
39
  config_override: Dict[str, Any] | None = None,
43
40
  ):
44
41
  initialize_port_ocean_context(self)
45
-
46
- self.starlette_app = app or Starlette()
47
- self.integration_router = integration_router or Router()
42
+ self.fast_api_app = app or FastAPI()
43
+ self.fast_api_app.middleware("http")(request_handler)
48
44
 
49
45
  self.config = IntegrationConfiguration(
50
46
  # type: ignore
@@ -56,6 +52,7 @@ class Ocean:
56
52
  sensitive_log_filter.hide_sensitive_strings(
57
53
  *self.config.get_sensitive_fields_data()
58
54
  )
55
+ self.integration_router = integration_router or APIRouter()
59
56
 
60
57
  self.port_client = PortClient(
61
58
  base_url=self.config.port.base_url,
@@ -80,20 +77,20 @@ class Ocean:
80
77
  self,
81
78
  ) -> None:
82
79
  async def execute_resync_all() -> None:
83
- # await self.resync_state_updater.update_before_resync()
80
+ await self.resync_state_updater.update_before_resync()
84
81
  logger.info("Starting a new scheduled resync")
85
82
  try:
86
83
  await self.integration.sync_raw_all()
87
- # await self.resync_state_updater.update_after_resync()
84
+ await self.resync_state_updater.update_after_resync()
88
85
  except asyncio.CancelledError:
89
86
  logger.warning(
90
87
  "resync was cancelled by the scheduled resync, skipping state update"
91
88
  )
92
- # except Exception as e:
93
- # await self.resync_state_updater.update_after_resync(
94
- # IntegrationStateStatus.Failed
95
- # )
96
- # raise e
89
+ except Exception as e:
90
+ await self.resync_state_updater.update_after_resync(
91
+ IntegrationStateStatus.Failed
92
+ )
93
+ raise e
97
94
 
98
95
  interval = self.config.scheduled_resync_interval
99
96
  loop = asyncio.get_event_loop()
@@ -105,7 +102,7 @@ class Ocean:
105
102
  repeated_function = repeat_every(
106
103
  seconds=interval * 60,
107
104
  # Not running the resync immediately because the event listener should run resync on startup
108
- wait_first=False,
105
+ wait_first=True,
109
106
  )(
110
107
  lambda: threading.Thread(
111
108
  target=lambda: asyncio.run_coroutine_threadsafe(
@@ -116,8 +113,10 @@ class Ocean:
116
113
  await repeated_function()
117
114
 
118
115
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
119
- @contextlib.asynccontextmanager
120
- async def lifespan(_: FastAPI) -> AsyncIterator[None]:
116
+ self.fast_api_app.include_router(self.integration_router, prefix="/integration")
117
+
118
+ @asynccontextmanager
119
+ async def lifecycle(_: FastAPI) -> AsyncIterator[None]:
121
120
  try:
122
121
  await self.integration.start()
123
122
  await self._setup_scheduled_resync()
@@ -128,16 +127,5 @@ class Ocean:
128
127
  finally:
129
128
  signal_handler.exit()
130
129
 
131
- async def health() -> JSONResponse:
132
- return JSONResponse({"ok": True})
133
-
134
- self.starlette_app = Starlette(
135
- routes=[
136
- Route("/docs", endpoint=health),
137
- Mount("/integration", routes=self.integration_router.routes),
138
- ],
139
- middleware=[Middleware(RequestHandlerMiddleware)],
140
- lifespan=lifespan,
141
- )
142
-
143
- await self.starlette_app(scope, receive, send)
130
+ self.fast_api_app.router.lifespan_context = lifecycle
131
+ await self.fast_api_app(scope, receive, send)
@@ -1,5 +1,5 @@
1
1
  from os import path
2
- from typing import Any, Callable, List, Tuple
2
+ from typing import Any, Callable, Dict, List, Tuple, Union
3
3
 
4
4
  import pytest
5
5
  import pytest_asyncio
@@ -30,8 +30,8 @@ def port_client_for_fake_integration() -> Tuple[SmokeTestDetails, PortClient]:
30
30
  def get_mocked_ocean_app(request: Any) -> Callable[[], Ocean]:
31
31
  test_dir = path.join(path.dirname(request.module.__file__), "..")
32
32
 
33
- def get_ocean_app() -> Ocean:
34
- return get_integration_ocean_app(test_dir)
33
+ def get_ocean_app(config_overrides: Union[Dict[str, Any], None] = None) -> Ocean:
34
+ return get_integration_ocean_app(test_dir, config_overrides)
35
35
 
36
36
  return get_ocean_app
37
37
 
@@ -1,7 +1,7 @@
1
1
  import sys
2
2
  from inspect import getmembers
3
3
  from pathlib import Path
4
- from typing import List, Tuple
4
+ from typing import Any, Dict, List, Tuple, Union
5
5
 
6
6
  from yaml import safe_load
7
7
 
@@ -12,7 +12,9 @@ from port_ocean.ocean import Ocean
12
12
  from port_ocean.utils.misc import get_spec_file, load_module
13
13
 
14
14
 
15
- def get_integration_ocean_app(integration_path: str) -> Ocean:
15
+ def get_integration_ocean_app(
16
+ integration_path: str, config_overrides: Union[Dict[str, Any], None] = None
17
+ ) -> Ocean:
16
18
  spec_file = get_spec_file(Path(integration_path))
17
19
 
18
20
  config_factory = None if not spec_file else spec_file.get("configurations", [])
@@ -21,9 +23,12 @@ def get_integration_ocean_app(integration_path: str) -> Ocean:
21
23
  integration_path,
22
24
  config_factory,
23
25
  {
24
- "port": {
25
- "client_id": "bla",
26
- "client_secret": "bla",
26
+ **(config_overrides or {}),
27
+ **{
28
+ "port": {
29
+ "client_id": "bla",
30
+ "client_secret": "bla",
31
+ },
27
32
  },
28
33
  },
29
34
  )
@@ -0,0 +1,45 @@
1
+ from typing import Any, AsyncGenerator
2
+ import asyncio
3
+ from port_ocean.utils.async_iterators import semaphore_async_iterator
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_semaphore_async_iterator() -> None:
9
+ max_concurrency = 5
10
+ semaphore = asyncio.BoundedSemaphore(max_concurrency)
11
+
12
+ concurrent_tasks = 0
13
+ max_concurrent_tasks = 0
14
+ lock = asyncio.Lock() # Protect shared variables
15
+
16
+ num_tasks = 20
17
+
18
+ async def mock_function() -> AsyncGenerator[str, None]:
19
+ nonlocal concurrent_tasks, max_concurrent_tasks
20
+
21
+ async with lock:
22
+ concurrent_tasks += 1
23
+ if concurrent_tasks > max_concurrent_tasks:
24
+ max_concurrent_tasks = concurrent_tasks
25
+
26
+ await asyncio.sleep(0.1)
27
+ yield "result"
28
+
29
+ async with lock:
30
+ concurrent_tasks -= 1
31
+
32
+ async def consume_iterator(async_iterator: Any) -> None:
33
+ async for _ in async_iterator:
34
+ pass
35
+
36
+ tasks = [
37
+ consume_iterator(semaphore_async_iterator(semaphore, mock_function))
38
+ for _ in range(num_tasks)
39
+ ]
40
+ await asyncio.gather(*tasks)
41
+
42
+ assert (
43
+ max_concurrent_tasks <= max_concurrency
44
+ ), f"Max concurrent tasks {max_concurrent_tasks} exceeded semaphore limit {max_concurrency}"
45
+ assert concurrent_tasks == 0, "Not all tasks have completed"
@@ -2,6 +2,9 @@ import typing
2
2
 
3
3
  import aiostream
4
4
 
5
+ if typing.TYPE_CHECKING:
6
+ from asyncio import Semaphore
7
+
5
8
 
6
9
  async def stream_async_iterators_tasks(
7
10
  *tasks: typing.AsyncIterable[typing.Any],
@@ -47,3 +50,60 @@ async def stream_async_iterators_tasks(
47
50
  async with combine.stream() as streamer:
48
51
  async for batch_items in streamer:
49
52
  yield batch_items
53
+
54
+
55
+ async def semaphore_async_iterator(
56
+ semaphore: "Semaphore",
57
+ function: typing.Callable[[], typing.AsyncIterator[typing.Any]],
58
+ ) -> typing.AsyncIterator[typing.Any]:
59
+ """
60
+ Executes an asynchronous iterator function under a semaphore to limit concurrency.
61
+
62
+ This function ensures that the provided asynchronous iterator function is executed
63
+ while respecting the concurrency limit imposed by the semaphore. It acquires the
64
+ semaphore before executing the function and releases it after the function completes,
65
+ thus controlling the number of concurrent executions.
66
+
67
+ Parameters:
68
+ semaphore (asyncio.Semaphore | asyncio.BoundedSemaphore): The semaphore used to limit concurrency.
69
+ function (Callable[[], AsyncIterator[Any]]): A nullary asynchronous function, - apply arguments with `functools.partial` or an anonymous function (lambda)
70
+ that returns an asynchronous iterator. This function is executed under the semaphore.
71
+
72
+ Yields:
73
+ Any: The items yielded by the asynchronous iterator function.
74
+
75
+ Usage:
76
+ ```python
77
+ import asyncio
78
+
79
+ async def async_iterator_function(param1, param2):
80
+ # Your async code here
81
+ yield ...
82
+
83
+ async def async_generator_function():
84
+ # Your async code to retrieve items
85
+ param1 = "your_param1"
86
+ yield param1
87
+
88
+ async def main():
89
+ semaphore = asyncio.BoundedSemaphore(50)
90
+ param2 = "your_param2"
91
+
92
+ tasks = [
93
+ semaphore_async_iterator(
94
+ semaphore,
95
+ lambda: async_iterator_function(param1, param2) # functools.partial(async_iterator_function, param1, param2)
96
+ )
97
+ async for param1 in async_generator_function()
98
+ ]
99
+
100
+ async for batch in stream_async_iterators_tasks(*tasks):
101
+ # Process each batch
102
+ pass
103
+
104
+ asyncio.run(main())
105
+ ```
106
+ """
107
+ async with semaphore:
108
+ async for result in function():
109
+ yield result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.12.2.dev22
3
+ Version: 0.12.4
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
@@ -16,7 +16,6 @@ Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
19
  Classifier: Topic :: Software Development :: Libraries
21
20
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -26,7 +25,7 @@ Requires-Dist: aiostream (>=0.5.2,<0.7.0)
26
25
  Requires-Dist: click (>=8.1.3,<9.0.0) ; extra == "cli"
27
26
  Requires-Dist: confluent-kafka (>=2.1.1,<3.0.0)
28
27
  Requires-Dist: cookiecutter (>=2.1.1,<3.0.0) ; extra == "cli"
29
- Requires-Dist: fastapi (>=0.100,<0.116)
28
+ Requires-Dist: fastapi (>=0.100,<0.112)
30
29
  Requires-Dist: httpx (>=0.24.1,<0.28.0)
31
30
  Requires-Dist: jinja2-time (>=0.2.0,<0.3.0) ; extra == "cli"
32
31
  Requires-Dist: jq (>=1.8.0,<2.0.0)
@@ -40,7 +39,7 @@ Requires-Dist: rich (>=13.4.1,<14.0.0) ; extra == "cli"
40
39
  Requires-Dist: six (>=1.16.0,<2.0.0)
41
40
  Requires-Dist: tomli (>=2.0.1,<3.0.0)
42
41
  Requires-Dist: urllib3 (>=1.26.16,<3.0.0)
43
- Requires-Dist: uvicorn (>=0.22,<0.33)
42
+ Requires-Dist: uvicorn (>=0.22,<0.31)
44
43
  Requires-Dist: werkzeug (>=2.3.4,<4.0.0)
45
44
  Project-URL: Repository, https://github.com/port-labs/Port-Ocean
46
45
  Description-Content-Type: text/markdown
@@ -9,14 +9,14 @@ port_ocean/cli/commands/defaults/dock.py,sha256=pFtHrU_LTvb5Ddrzj09Wxy-jg1Ym10wB
9
9
  port_ocean/cli/commands/defaults/group.py,sha256=hii_4CYoQ7jSMePbnP4AmruO_RKWCUcoV7dXXBlZafc,115
10
10
  port_ocean/cli/commands/list_integrations.py,sha256=DVVioFruGUE-_v6UUHlcemWNN6RlWwCrf1X4HmAXsf8,1134
11
11
  port_ocean/cli/commands/main.py,sha256=gj0lmuLep2XeLNuabB7Wk0UVYPT7_CD_rAw5AoUQWSE,1057
12
- port_ocean/cli/commands/new.py,sha256=uNDzb2cmUdOHBGsBujWmlB9FrlJvB8CD9dnXY_btGUc,3777
12
+ port_ocean/cli/commands/new.py,sha256=39_RnEZHoY0pWf59z_oXrLXAyVwzmFY0XcpCJrtZINI,3496
13
13
  port_ocean/cli/commands/pull.py,sha256=VvrRjLNlfPuLIf7KzeIcbzzdi98Z0M9wCRpXC3QPxdI,2306
14
14
  port_ocean/cli/commands/sail.py,sha256=rY7rEMjfy_KXiWvtL0T72TTLgeQ3HW4SOzKkz9wL9nI,2282
15
15
  port_ocean/cli/commands/version.py,sha256=hEuIEIcm6Zkamz41Z9nxeSM_4g3oNlAgWwQyDGboh-E,536
16
16
  port_ocean/cli/cookiecutter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  port_ocean/cli/cookiecutter/cookiecutter.json,sha256=N5UrAP2e5JbgEDz_WTQFIZlzSveME6x32sHeA7idjh0,481
18
18
  port_ocean/cli/cookiecutter/extensions.py,sha256=eQNjZvy2enDkJpvMbBGil77Xk9-38f862wfnmCjdoBc,446
19
- port_ocean/cli/cookiecutter/hooks/post_gen_project.py,sha256=N-gcNPhVSMGBPI69etVU2QgyhvdzaxNMvUZbyYSdVM4,413
19
+ port_ocean/cli/cookiecutter/hooks/post_gen_project.py,sha256=xZbDPSmfP-ZXNlPaqQDsYLuNfdhFpLX9fIshiAd94Qg,535
20
20
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore,sha256=9Mz_WI7XBpKzlJ7ILb4vlcuzYkh98Ql3bP_5GHN1sRY,1034
21
21
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example,sha256=LnNPRe3RnzjWPL4tNLYEQiMvFEZHSy3ceqwQEapcpwE,92
22
22
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore,sha256=32p1lDW_g5hyBz486GWfDeR9m7ikFlASVri5a8vmNoo,2698
@@ -57,7 +57,7 @@ port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
57
57
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
58
58
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  port_ocean/context/event.py,sha256=WduGbCPgm2J2a63EY4J3XWwFGSt3ja1acBVpyI_ciMo,5430
60
- port_ocean/context/ocean.py,sha256=UDlsk6CUCRLb0Oc01piU3CdtG9LpBcw-yswk8DtiDAw,4661
60
+ port_ocean/context/ocean.py,sha256=2EreWOj-N2H7QUjEt5wGiv5KHP4pTZc70tn_wHcpF4w,4657
61
61
  port_ocean/context/resource.py,sha256=yDj63URzQelj8zJPh4BAzTtPhpKr9Gw9DRn7I_0mJ1s,1692
62
62
  port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  port_ocean/core/defaults/__init__.py,sha256=8qCZg8n06WAdMu9s_FiRtDYLGPGHbOuS60vapeUoAks,142
@@ -89,18 +89,18 @@ port_ocean/core/handlers/port_app_config/models.py,sha256=YvYtf_44KD_rN4xK-3xHtd
89
89
  port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
90
90
  port_ocean/core/handlers/resync_state_updater/updater.py,sha256=Yg9ET6ZV5B9GW7u6zZA6GlB_71kmvxvYX2FWgQNzMvo,3182
91
91
  port_ocean/core/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
- port_ocean/core/integrations/base.py,sha256=3nGOgJHzzlhr1OlrebzDENDAG6tiOyJfnijWFYtPn1U,3012
92
+ port_ocean/core/integrations/base.py,sha256=JxqQApEf7phi_Q_b4U0mKNomxIPdDyKPq3AbApFmhjU,3007
93
93
  port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4_RgZnwSRmTCreKcBVM,220
94
94
  port_ocean/core/integrations/mixins/events.py,sha256=Ddfx2L4FpghV38waF8OfVeOV0bHBxNIgjU-q5ffillI,2341
95
95
  port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
96
96
  port_ocean/core/integrations/mixins/sync.py,sha256=B9fEs8faaYLLikH9GBjE_E61vo0bQDjIGQsQ1SRXOlA,3931
97
- port_ocean/core/integrations/mixins/sync_raw.py,sha256=tZFWCPthhSaQ1x2TsBDnSN_G_WgzYVX0xE6kBvDCtHc,19000
97
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=BGS5EnZ2N3ifcAi94Wo-ZassSJ-_Se9eFJMpBDT7pNY,18841
98
98
  port_ocean/core/integrations/mixins/utils.py,sha256=7y1rGETZIjOQadyIjFJXIHKkQFKx_SwiP-TrAIsyyLY,2303
99
99
  port_ocean/core/models.py,sha256=dJ2_olTdbjUpObQJNmg7e7EENU_zZiX6XOaknNp54B0,1342
100
100
  port_ocean/core/ocean_types.py,sha256=3_d8-n626f1kWLQ_Jxw194LEyrOVupz05qs_Y1pvB-A,990
101
101
  port_ocean/core/utils.py,sha256=40UjRauRJO47WDSNn9bkCRD2bfhfB3e-dnOLULnuVzE,3631
102
102
  port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
103
- port_ocean/exceptions/api.py,sha256=gb2jqBfRv21-Y5BQ8-1DmYMT7uv7QEhhNDvpZCvqdKU,428
103
+ port_ocean/exceptions/api.py,sha256=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
104
104
  port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
105
105
  port_ocean/exceptions/clients.py,sha256=LKLLs-Zy3caNG85rwxfOw2rMr8qqVV6SHUq4fRCZ99U,180
106
106
  port_ocean/exceptions/context.py,sha256=mA8HII6Rl4QxKUz98ppy1zX3kaziaen21h1ZWuU3ADc,372
@@ -112,10 +112,10 @@ port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861v
112
112
  port_ocean/helpers/retry.py,sha256=IQ0RfQ2T5o6uoZh2WW2nrFH5TT6K_k3y2Im0HDp5j9Y,15059
113
113
  port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
114
  port_ocean/log/handlers.py,sha256=k9G_Mb4ga2-Jke9irpdlYqj6EYiwv0gEsh4TgyqqOmI,2853
115
- port_ocean/log/logger_setup.py,sha256=qSeVwnivV4WoLx_4SfBwn2PtmUpNdkSEgfm0C8B3yUw,2332
115
+ port_ocean/log/logger_setup.py,sha256=BaXt-mh9CVXhneh37H46d04lqOdIBixG1pFyGfotuZs,2328
116
116
  port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2920
117
- port_ocean/middlewares.py,sha256=K_FGt39YgiC0397W3ON1Z0n0bRIke95sZkw7a0xOiII,2737
118
- port_ocean/ocean.py,sha256=Vcc2x2d8-JiipX0xLeMX8p30SUzXfrRMlylwjFZY0pM,5394
117
+ port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
118
+ port_ocean/ocean.py,sha256=Oe4H3kKtkj52uNO4Rd_47iY3MBdrTtshXZ_16q7A8bM,5071
119
119
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
120
  port_ocean/run.py,sha256=rTxBlrQd4yyrtgErCFJCHCEHs7d1OXrRiJehUYmIbN0,2212
121
121
  port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
@@ -124,15 +124,16 @@ port_ocean/tests/clients/port/mixins/test_entities.py,sha256=A9myrnkLhKSQrnOLv1Z
124
124
  port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
125
125
  port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=Yv03P-LDcJCKZ21exiTFrcT1eu0zn6Z954dilxrb52Y,10842
126
126
  port_ocean/tests/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
- port_ocean/tests/helpers/fixtures.py,sha256=blc4ZgPEkKOmmwT6GVxceS9r1ERUwSdOIBGxWFwvRyY,1398
127
+ port_ocean/tests/helpers/fixtures.py,sha256=IQEplbHhRgjrAsZlnXrgSYA5YQEn25I9HgO3_Fjibxg,1481
128
128
  port_ocean/tests/helpers/integration.py,sha256=_RxS-RHpu11lrbhUXYPZp862HLWx8AoD7iZM6iXN8rs,1104
129
- port_ocean/tests/helpers/ocean_app.py,sha256=Dp1bwEDhWsx_G-KVxOfJX1eVIS4168ajLu39wAY275g,1693
129
+ port_ocean/tests/helpers/ocean_app.py,sha256=NYDno5RKBcjSVJUmnmHZVmRDcGmFRoyoBxKt2ls699w,1858
130
130
  port_ocean/tests/helpers/port_client.py,sha256=5d6GNr8vNNSOkrz1AdOhxBUKuusr_-UPDP7AVpHasQw,599
131
131
  port_ocean/tests/helpers/smoke_test.py,sha256=_9aJJFRfuGJEg2D2YQJVJRmpreS6gEPHHQq8Q01x4aQ,2697
132
132
  port_ocean/tests/test_smoke.py,sha256=uix2uIg_yOm8BHDgHw2hTFPy1fiIyxBGW3ENU_KoFlo,2557
133
+ port_ocean/tests/utils/test_async_iterators.py,sha256=3PLk1emEXekb8LcC5GgVh3OicaX15i5WyaJT_eFnu_4,1336
133
134
  port_ocean/utils/__init__.py,sha256=KMGnCPXZJbNwtgxtyMycapkDz8tpSyw23MSYT3iVeHs,91
134
135
  port_ocean/utils/async_http.py,sha256=arnH458TExn2Dju_Sy6pHas_vF5RMWnOp-jBz5WAAcE,1226
135
- port_ocean/utils/async_iterators.py,sha256=iw3cUHxfQm3zUSPdw2FmSXDU8E1Ppnys4TGhswNuQ8s,1569
136
+ port_ocean/utils/async_iterators.py,sha256=CPXskYWkhkZtAG-ducEwM8537t3z5usPEqXR9vcivzw,3715
136
137
  port_ocean/utils/cache.py,sha256=3KItZDE2yVrbVDr-hoM8lNna8s2dlpxhP4ICdLjH4LQ,2231
137
138
  port_ocean/utils/misc.py,sha256=0q2cJ5psqxn_5u_56pT7vOVQ3shDM02iC1lzyWQ_zl0,2098
138
139
  port_ocean/utils/queue_utils.py,sha256=KWWl8YVnG-glcfIHhM6nefY-2sou_C6DVP1VynQwzB4,2762
@@ -140,8 +141,8 @@ port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,32
140
141
  port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
141
142
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
142
143
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
143
- port_ocean-0.12.2.dev22.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
144
- port_ocean-0.12.2.dev22.dist-info/METADATA,sha256=l5UuI6nWegX85qi5OXaFjyA7kllf6sI-uhzr8fPpCr4,6671
145
- port_ocean-0.12.2.dev22.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
146
- port_ocean-0.12.2.dev22.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
147
- port_ocean-0.12.2.dev22.dist-info/RECORD,,
144
+ port_ocean-0.12.4.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
145
+ port_ocean-0.12.4.dist-info/METADATA,sha256=sguRwwu52e3UgwQimCYFM3jiqEscIToBDFefaIdiWgs,6614
146
+ port_ocean-0.12.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
147
+ port_ocean-0.12.4.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
148
+ port_ocean-0.12.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any