port-ocean 0.9.14__py3-none-any.whl → 0.10.1__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.

Files changed (31) hide show
  1. port_ocean/cli/commands/new.py +38 -4
  2. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +2 -0
  3. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +41 -0
  4. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +16 -0
  5. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +6 -6
  6. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +1 -1
  7. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +7 -0
  8. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile +3 -3
  9. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +1 -0
  10. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +16 -1
  11. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +7 -3
  12. port_ocean/clients/port/client.py +17 -0
  13. port_ocean/clients/port/mixins/entities.py +1 -1
  14. port_ocean/config/settings.py +9 -8
  15. port_ocean/core/event_listener/base.py +37 -0
  16. port_ocean/core/event_listener/http.py +1 -1
  17. port_ocean/core/event_listener/kafka.py +8 -4
  18. port_ocean/core/event_listener/once.py +96 -1
  19. port_ocean/core/event_listener/polling.py +14 -10
  20. port_ocean/core/handlers/resync_state_updater/__init__.py +5 -0
  21. port_ocean/core/handlers/resync_state_updater/updater.py +84 -0
  22. port_ocean/core/models.py +5 -2
  23. port_ocean/core/utils.py +3 -2
  24. port_ocean/ocean.py +25 -7
  25. port_ocean/utils/misc.py +7 -1
  26. port_ocean/utils/time.py +54 -0
  27. {port_ocean-0.9.14.dist-info → port_ocean-0.10.1.dist-info}/METADATA +1 -1
  28. {port_ocean-0.9.14.dist-info → port_ocean-0.10.1.dist-info}/RECORD +31 -24
  29. {port_ocean-0.9.14.dist-info → port_ocean-0.10.1.dist-info}/LICENSE.md +0 -0
  30. {port_ocean-0.9.14.dist-info → port_ocean-0.10.1.dist-info}/WHEEL +0 -0
  31. {port_ocean-0.9.14.dist-info → port_ocean-0.10.1.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,39 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  import click
4
+ import json
4
5
  from cookiecutter.main import cookiecutter # type: ignore
6
+ import os
5
7
 
6
8
  from port_ocean.cli.commands.main import cli_start, print_logo, console
7
9
  from port_ocean.cli.utils import cli_root_path
8
10
 
9
11
 
12
+ def add_vscode_configuration(result: str, name: str) -> None:
13
+ vscode_entry_root_path = "${workspaceFolder}/integrations/" + name
14
+ new_vscode_entry = {
15
+ "console": "integratedTerminal",
16
+ "cwd": vscode_entry_root_path,
17
+ "envFile": f"{vscode_entry_root_path}/.env",
18
+ "justMyCode": True,
19
+ "name": f"Run {name} integration",
20
+ "program": f"{vscode_entry_root_path}/debug.py",
21
+ "python": f"{vscode_entry_root_path}/.venv/bin/python",
22
+ "request": "launch",
23
+ "type": "debugpy",
24
+ }
25
+
26
+ vs_code_json_path = os.path.join(os.path.dirname(result), "../.vscode/launch.json")
27
+ if not os.path.exists(vs_code_json_path):
28
+ return
29
+ vs_code_json = json.load(open(vs_code_json_path, "r"))
30
+ vs_code_json["configurations"].append(new_vscode_entry)
31
+
32
+ with open(vs_code_json_path, "w") as vs_code_json_file:
33
+ json.dump(vs_code_json, vs_code_json_file, indent=2)
34
+ vs_code_json_file.write("\n")
35
+
36
+
10
37
  @cli_start.command()
11
38
  @click.argument("path", default=".", type=click.Path(exists=True))
12
39
  @click.option(
@@ -37,6 +64,9 @@ def new(path: str, is_private_integration: bool) -> None:
37
64
  )
38
65
  name = result.split("/")[-1]
39
66
 
67
+ if not is_private_integration:
68
+ add_vscode_configuration(result, name)
69
+
40
70
  console.print(
41
71
  "\n🌊 Ahoy, Captain! Your project is ready to set sail into the vast ocean of possibilities!",
42
72
  style="bold",
@@ -47,10 +77,14 @@ def new(path: str, is_private_integration: bool) -> None:
47
77
  f"▶️ [bold][blue]cd {path}/{name} && make install && . .venv/bin/activate[/blue][/bold]\n"
48
78
  )
49
79
  console.print(
50
- "⚓️ Set sail with [blue]Ocean[/blue]: Run [bold][blue]ocean sail[/blue] <path_to_integration>[/bold] to run the project using Ocean.\n"
51
- f"▶️ [bold][blue]ocean sail {path}/{name}[/blue][/bold] \n"
80
+ f"⚓️ Copy example env file: Run [bold][blue]cp {path}/{name}.env.example {path}/{name}/.env [/blue][/bold] and set your port credentials in the created file.\n"
52
81
  )
53
82
  console.print(
54
- "⚓️ Smooth sailing with [blue]Make[/blue]: Alternatively, you can run [bold][blue]make run[/blue][/bold] to launch your project using Make. \n"
55
- f"▶️ [bold][blue]make run {path}/{name}[/blue][/bold]"
83
+ "⚓️ Set sail with [blue]Ocean[/blue]: Run [bold][blue]ocean sail[/blue] <path_to_integration>[/bold] to run the project using Ocean.\n"
84
+ f"▶️ [bold][blue]ocean sail {path}/{name}[/blue][/bold] \n"
56
85
  )
86
+ if not is_private_integration:
87
+ console.print(
88
+ "⚓️ Smooth sailing with [blue]Make[/blue]: Alternatively, you can run [bold][blue]make run[/blue][/bold] to launch your project using Make. \n"
89
+ f"▶️ [bold][blue]make run {path}/{name}[/blue][/bold]"
90
+ )
@@ -0,0 +1,2 @@
1
+ OCEAN__PORT__CLIENT_ID="<port-client-id>"
2
+ OCEAN__PORT__CLIENT_SECRET="<port-client-secret>"
@@ -0,0 +1,41 @@
1
+ [
2
+ {
3
+ "identifier": "{{ cookiecutter.integration_slug }}ExampleBlueprint",
4
+ "title": "{{ cookiecutter.integration_name }} Example",
5
+ "icon": "Blueprint",
6
+ "schema": {
7
+ "properties": {
8
+ "status": {
9
+ "type": "string",
10
+ "enum": [
11
+ "VALID",
12
+ "FAILED"
13
+ ],
14
+ "enumColors": {
15
+ "VALID": "green",
16
+ "FAILED": "red"
17
+ },
18
+ "title": "Status"
19
+ },
20
+ "text": {
21
+ "type": "string",
22
+ "title": "Text"
23
+ },
24
+ "component": {
25
+ "type": "string",
26
+ "title": "Component"
27
+ },
28
+ "service": {
29
+ "type": "string",
30
+ "title": "Service"
31
+ },
32
+ "score": {
33
+ "type": "number",
34
+ "title": "Score"
35
+ }
36
+ },
37
+ "required": []
38
+ },
39
+ "relations": {}
40
+ }
41
+ ]
@@ -0,0 +1,16 @@
1
+ resources:
2
+ - kind: {{ cookiecutter.integration_slug}}-example-kind
3
+ selector:
4
+ query: 'true'
5
+ port:
6
+ entity:
7
+ mappings:
8
+ identifier: .my_custom_id
9
+ title: '(.my_component + " @ " + .my_service)'
10
+ blueprint: '"{{ cookiecutter.integration_slug }}ExampleBlueprint"'
11
+ properties:
12
+ status: .my_enum
13
+ text: .my_custom_text
14
+ component: .my_component
15
+ service: .my_service
16
+ score: .my_special_score
@@ -1,14 +1,14 @@
1
1
  description: {{cookiecutter.integration_name}} integration for Port Ocean
2
- icon: Cookiecutter # Should be one of the available icons in Port
2
+ icon: Cookiecutter # Should be one of the available icons in Port
3
3
  features:
4
4
  - type: exporter
5
- section: Git Providers # Should be one of the available sections in Port
5
+ section: Under Development # Should be one of the available sections in Port
6
6
  resources:
7
- - kind: <ResourceName1>
8
- - kind: <ResourceName2>
7
+ - kind: {{ cookiecutter.integration_slug }}-example-kind
8
+ # - kind: <ResourceName2>
9
9
  configurations:
10
- - name: myGitToken
11
- required: true
10
+ - name: my{{ cookiecutter.integration_slug}}Token
11
+ # required: true
12
12
  type: string
13
13
  sensitive: true
14
14
  - name: someApplicationUrl
@@ -1,4 +1,4 @@
1
- # Changelog
1
+ # Changelog - Ocean - {{ cookiecutter.integration_slug }}
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
@@ -0,0 +1,7 @@
1
+ # Contributing to Ocean - {{ cookiecutter.integration_slug }}
2
+
3
+ ## Running locally
4
+
5
+ #### NOTE: Add your own instructions of how to run {{ cookiecutter.integration_slug }}
6
+
7
+ This could be any gotcha's such as rate limiting, how to setup credentials and so forth
@@ -55,8 +55,8 @@ lint:
55
55
  run:
56
56
  $(ACTIVATE) && ocean sail
57
57
 
58
- test: lint
59
- $(ACTIVATE) && poetry run pytest
58
+ test:
59
+ $(ACTIVATE) && poetry run pytest -n auto
60
60
 
61
61
  clean:
62
62
  @find . -name '.venv' -type d -exec rm -rf {} \;
@@ -71,4 +71,4 @@ clean:
71
71
  rm -rf htmlcov
72
72
  rm -rf .tox/
73
73
  rm -rf docs/_build
74
- rm -rf dist/
74
+ rm -rf dist/
@@ -15,6 +15,21 @@ async def on_resync(kind: str) -> list[dict[Any, Any]]:
15
15
  # return [{"some_project_key": "someProjectValue", ...}]
16
16
  # if kind == "issues":
17
17
  # return [{"some_issue_key": "someIssueValue", ...}]
18
+
19
+ # Initial stub to show complete flow, replace this with your own logic
20
+ if kind == "{{ cookiecutter.integration_slug }}-example-kind":
21
+ return [
22
+ {
23
+ "my_custom_id": f"id_{x}",
24
+ "my_custom_text": f"very long text with {x} in it",
25
+ "my_special_score": x * 32 % 3,
26
+ "my_component": f"component-{x}",
27
+ "my_service": f"service-{x %2}",
28
+ "my_enum": "VALID" if x % 2 == 0 else "FAILED",
29
+ }
30
+ for x in range(25)
31
+ ]
32
+
18
33
  return []
19
34
 
20
35
 
@@ -38,4 +53,4 @@ async def on_resync(kind: str) -> list[dict[Any, Any]]:
38
53
  async def on_start() -> None:
39
54
  # Something to do when the integration starts
40
55
  # For example create a client to query 3rd party services - GitHub, Jira, etc...
41
- print("Starting integration")
56
+ print("Starting {{ cookiecutter.integration_slug }} integration")
@@ -9,13 +9,17 @@ python = "^3.11"
9
9
  port_ocean = { version = "^{% version %}", extras = ["cli"] }
10
10
 
11
11
  [tool.poetry.group.dev.dependencies]
12
+ # Uncomment this if you want to debug the ocean core together with your integration
13
+ # port_ocean = { path = '../../', develop = true, extras = ['all'] }
12
14
  pytest = "^7.2"
15
+ pytest-xdist = "^3.6.1"
16
+ pre-commit = "^3.7.1"
17
+ requests = "^2.32.3"
13
18
  black = "^23.3.0"
14
19
  mypy = "^1.3.0"
15
20
  ruff = "^0.0.278"
16
21
  pylint = "^2.17.4"
17
22
  towncrier = "^23.6.0"
18
- pytest-xdist = "^3.6.1"
19
23
 
20
24
  [tool.towncrier]
21
25
  directory = "changelog"
@@ -54,8 +58,8 @@ underlines = [""]
54
58
  showcontent = true
55
59
 
56
60
  [build-system]
57
- requires = ["poetry>=0.12"]
58
- build-backend = "poetry.masonry.api"
61
+ requires = ["poetry-core>=1.0.0"]
62
+ build-backend = "poetry.core.masonry.api"
59
63
 
60
64
  [tool.mypy]
61
65
  exclude = [
@@ -13,6 +13,7 @@ from port_ocean.clients.port.utils import (
13
13
  get_internal_http_client,
14
14
  )
15
15
  from port_ocean.exceptions.clients import KafkaCredentialsNotFound
16
+ from typing import Any
16
17
 
17
18
 
18
19
  class PortClient(
@@ -75,3 +76,19 @@ class PortClient(
75
76
  handle_status_code(response)
76
77
 
77
78
  return response.json()["organization"]["id"]
79
+
80
+ async def update_integration_state(
81
+ self, state: dict[str, Any], should_raise: bool = True, should_log: bool = True
82
+ ) -> dict[str, Any]:
83
+ if should_log:
84
+ logger.debug(f"Updating integration resync state with: {state}")
85
+ response = await self.client.patch(
86
+ f"{self.api_url}/integration/{self.integration_identifier}/resync-state",
87
+ headers=await self.auth.headers(),
88
+ json=state,
89
+ )
90
+ handle_status_code(response, should_raise, should_log)
91
+ if response.is_success and should_log:
92
+ logger.info("Integration resync state updated successfully")
93
+
94
+ return response.json().get("integration", {})
@@ -58,7 +58,7 @@ class EntityClientMixin:
58
58
  )
59
59
  handle_status_code(response, should_raise)
60
60
  result = response.json()
61
- result_entity = Entity.parse_obj(result)
61
+ result_entity = Entity.parse_obj(result["entity"])
62
62
  # Set the results of the search relation and identifier to the entity
63
63
  entity.identifier = result_entity.identifier or entity.identifier
64
64
  entity.relations = result_entity.relations or entity.relations
@@ -1,4 +1,4 @@
1
- from typing import Any, Literal, Type
1
+ from typing import Any, Literal, Type, cast
2
2
 
3
3
  from pydantic import Extra, AnyHttpUrl, parse_obj_as, parse_raw_as
4
4
  from pydantic.class_validators import root_validator, validator
@@ -55,9 +55,8 @@ class IntegrationSettings(BaseOceanModel, extra=Extra.allow):
55
55
  integ_type = get_integration_name()
56
56
 
57
57
  values["type"] = integ_type.lower() if integ_type else None
58
- values["identifier"] = values.get(
59
- "identifier", f"my-{integ_type}-integration".lower()
60
- )
58
+ if not values.get("identifier"):
59
+ values["identifier"] = f"my-{integ_type}-integration".lower()
61
60
 
62
61
  return values
63
62
 
@@ -71,12 +70,14 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
71
70
  client_timeout: int = 30
72
71
  send_raw_data_examples: bool = True
73
72
  port: PortSettings
74
- event_listener: EventListenerSettingsType
73
+ event_listener: EventListenerSettingsType = Field(
74
+ default=cast(EventListenerSettingsType, {"type": "POLLING"})
75
+ )
75
76
  # If an identifier or type is not provided, it will be generated based on the integration name
76
77
  integration: IntegrationSettings = Field(
77
78
  default_factory=lambda: IntegrationSettings(type="", identifier="")
78
79
  )
79
- runtime: Runtime = "OnPrem"
80
+ runtime: Runtime = Runtime.OnPrem
80
81
 
81
82
  @root_validator()
82
83
  def validate_integration_config(cls, values: dict[str, Any]) -> dict[str, Any]:
@@ -100,8 +101,8 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
100
101
  return values
101
102
 
102
103
  @validator("runtime")
103
- def validate_runtime(cls, runtime: Literal["OnPrem", "Saas"]) -> Runtime:
104
- if runtime == "Saas":
104
+ def validate_runtime(cls, runtime: Runtime) -> Runtime:
105
+ if runtime == Runtime.Saas:
105
106
  spec = get_spec_file()
106
107
  if spec is None:
107
108
  raise ValueError(
@@ -5,6 +5,8 @@ from pydantic import Extra
5
5
 
6
6
  from port_ocean.config.base import BaseOceanModel
7
7
  from port_ocean.utils.signal import signal_handler
8
+ from port_ocean.context.ocean import ocean
9
+ from port_ocean.utils.misc import IntegrationStateStatus
8
10
 
9
11
 
10
12
  class EventListenerEvents(TypedDict):
@@ -36,6 +38,41 @@ class BaseEventListener:
36
38
  """
37
39
  pass
38
40
 
41
+ async def _before_resync(self) -> None:
42
+ """
43
+ Can be used for event listeners that need to perform some action before resync.
44
+ """
45
+ await ocean.app.resync_state_updater.update_before_resync()
46
+
47
+ async def _after_resync(self) -> None:
48
+ """
49
+ Can be used for event listeners that need to perform some action after resync.
50
+ """
51
+ await ocean.app.resync_state_updater.update_after_resync()
52
+
53
+ async def _on_resync_failure(self, e: Exception) -> None:
54
+ """
55
+ Can be used for event listeners that need to handle resync failures.
56
+ """
57
+ await ocean.app.resync_state_updater.update_after_resync(
58
+ IntegrationStateStatus.Failed
59
+ )
60
+
61
+ async def _resync(
62
+ self,
63
+ resync_args: dict[Any, Any],
64
+ ) -> None:
65
+ """
66
+ Triggers the "on_resync" event.
67
+ """
68
+ await self._before_resync()
69
+ try:
70
+ await self.events["on_resync"](resync_args)
71
+ await self._after_resync()
72
+ except Exception as e:
73
+ await self._on_resync_failure(e)
74
+ raise e
75
+
39
76
 
40
77
  class EventListenerSettings(BaseOceanModel, extra=Extra.allow):
41
78
  type: str
@@ -64,6 +64,6 @@ class HttpEventListener(BaseEventListener):
64
64
 
65
65
  @target_channel_router.post("/resync")
66
66
  async def resync() -> None:
67
- await self.events["on_resync"]({})
67
+ await self._resync({})
68
68
 
69
69
  ocean.app.fast_api_app.include_router(target_channel_router)
@@ -99,9 +99,13 @@ class KafkaEventListener(BaseEventListener):
99
99
  return False
100
100
 
101
101
  integration_identifier = after.get("identifier")
102
- if integration_identifier == self.integration_identifier and (
103
- "change.log" in topic
104
- ):
102
+ if integration_identifier != self.integration_identifier:
103
+ return False
104
+
105
+ if after.get("updatedAt") == after.get("resyncState", {}).get("updatedAt"):
106
+ return False
107
+
108
+ if "change.log" in topic:
105
109
  return msg_value.get("changelogDestination", {}).get("type", "") == "KAFKA"
106
110
 
107
111
  return False
@@ -122,7 +126,7 @@ class KafkaEventListener(BaseEventListener):
122
126
 
123
127
  if "change.log" in topic and message is not None:
124
128
  try:
125
- await self.events["on_resync"](message)
129
+ await self._resync(message)
126
130
  except Exception as e:
127
131
  _type, _, tb = sys.exc_info()
128
132
  logger.opt(exception=(_type, None, tb)).error(
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import signal
2
3
  from typing import Literal, Any
3
4
 
@@ -9,6 +10,9 @@ from port_ocean.core.event_listener.base import (
9
10
  EventListenerSettings,
10
11
  )
11
12
  from port_ocean.utils.repeat import repeat_every
13
+ from port_ocean.context.ocean import ocean
14
+ from port_ocean.utils.time import convert_str_to_utc_datetime, convert_to_minutes
15
+ from port_ocean.utils.misc import IntegrationStateStatus
12
16
 
13
17
 
14
18
  class OnceEventListenerSettings(EventListenerSettings):
@@ -41,6 +45,97 @@ class OnceEventListener(BaseEventListener):
41
45
  ):
42
46
  super().__init__(events)
43
47
  self.event_listener_config = event_listener_config
48
+ self.cached_integration: dict[str, Any] | None = None
49
+
50
+ async def get_current_integration_cached(self) -> dict[str, Any]:
51
+ if self.cached_integration:
52
+ return self.cached_integration
53
+
54
+ self.cached_integration = await ocean.port_client.get_current_integration()
55
+ return self.cached_integration
56
+
57
+ async def get_saas_resync_initialization_and_interval(
58
+ self,
59
+ ) -> tuple[int | None, datetime.datetime | None]:
60
+ """
61
+ Get the scheduled resync interval and the last updated time of the integration config for the saas application.
62
+ interval is the saas configured resync interval time.
63
+ start_time is the last updated time of the integration config.
64
+ return: (interval, start_time)
65
+ """
66
+ if not ocean.app.is_saas():
67
+ return (None, None)
68
+
69
+ try:
70
+ integration = await self.get_current_integration_cached()
71
+ except Exception as e:
72
+ logger.exception(f"Error occurred while getting current integration {e}")
73
+ return (None, None)
74
+
75
+ interval_str = (
76
+ integration.get("spec", {})
77
+ .get("appSpec", {})
78
+ .get("scheduledResyncInterval")
79
+ )
80
+
81
+ if not interval_str:
82
+ logger.error(
83
+ "Unexpected: scheduledResyncInterval not found for Saas integration, Cannot predict the next resync"
84
+ )
85
+ return (None, None)
86
+
87
+ last_updated_saas_integration_config_str = integration.get(
88
+ "statusInfo", {}
89
+ ).get("updatedAt")
90
+
91
+ # we use the last updated time of the integration config as the start time since in saas application the interval is configured by the user from the portal
92
+ if not last_updated_saas_integration_config_str:
93
+ logger.error(
94
+ "Unexpected: updatedAt not found for Saas integration, Cannot predict the next resync"
95
+ )
96
+ return (None, None)
97
+
98
+ return (
99
+ convert_to_minutes(interval_str),
100
+ convert_str_to_utc_datetime(last_updated_saas_integration_config_str),
101
+ )
102
+
103
+ async def _before_resync(self) -> None:
104
+ if not ocean.app.is_saas():
105
+ # in case of non-saas, we still want to update the state before and after the resync
106
+ await super()._before_resync()
107
+ return
108
+
109
+ (interval, start_time) = (
110
+ await self.get_saas_resync_initialization_and_interval()
111
+ )
112
+ await ocean.app.resync_state_updater.update_before_resync(interval, start_time)
113
+
114
+ async def _after_resync(self) -> None:
115
+ if not ocean.app.is_saas():
116
+ # in case of non-saas, we still want to update the state before and after the resync
117
+ await super()._after_resync()
118
+ return
119
+
120
+ (interval, start_time) = (
121
+ await self.get_saas_resync_initialization_and_interval()
122
+ )
123
+ await ocean.app.resync_state_updater.update_after_resync(
124
+ IntegrationStateStatus.Completed, interval, start_time
125
+ )
126
+
127
+ async def _on_resync_failure(self, e: Exception) -> None:
128
+ if not ocean.app.is_saas():
129
+ # in case of non-saas, we still want to update the state before and after the resync
130
+ await super()._after_resync()
131
+ return
132
+
133
+ (interval, start_time) = (
134
+ await self.get_saas_resync_initialization_and_interval()
135
+ )
136
+ await ocean.app.resync_state_updater.update_after_resync(
137
+ IntegrationStateStatus.Failed, interval, start_time
138
+ )
44
139
 
45
140
  async def _start(self) -> None:
46
141
  """
@@ -53,7 +148,7 @@ class OnceEventListener(BaseEventListener):
53
148
  async def resync_and_exit() -> None:
54
149
  logger.info("Once event listener started")
55
150
  try:
56
- await self.events["on_resync"]({})
151
+ await self._resync({})
57
152
  except Exception:
58
153
  # we catch all exceptions here to make sure the application will exit gracefully
59
154
  logger.exception("Error occurred while resyncing")
@@ -49,7 +49,16 @@ class PollingEventListener(BaseEventListener):
49
49
  ):
50
50
  super().__init__(events)
51
51
  self.event_listener_config = event_listener_config
52
- self._last_updated_at = None
52
+
53
+ def should_resync(self, last_updated_at: str) -> bool:
54
+ _last_updated_at = (
55
+ ocean.app.resync_state_updater.last_integration_state_updated_at
56
+ )
57
+
58
+ if _last_updated_at is None:
59
+ return self.event_listener_config.resync_on_start
60
+
61
+ return _last_updated_at != last_updated_at
53
62
 
54
63
  async def _start(self) -> None:
55
64
  """
@@ -69,17 +78,12 @@ class PollingEventListener(BaseEventListener):
69
78
  integration = await ocean.app.port_client.get_current_integration()
70
79
  last_updated_at = integration["updatedAt"]
71
80
 
72
- should_resync = (
73
- self._last_updated_at is not None
74
- or self.event_listener_config.resync_on_start
75
- ) and self._last_updated_at != last_updated_at
76
-
77
- if should_resync:
81
+ if self.should_resync(last_updated_at):
78
82
  logger.info("Detected change in integration, resyncing")
79
- self._last_updated_at = last_updated_at
80
- running_task: Task[Any] = get_event_loop().create_task(
81
- self.events["on_resync"]({}) # type: ignore
83
+ ocean.app.resync_state_updater.last_integration_state_updated_at = (
84
+ last_updated_at
82
85
  )
86
+ running_task: Task[Any] = get_event_loop().create_task(self._resync({}))
83
87
  signal_handler.register(running_task.cancel)
84
88
 
85
89
  await running_task
@@ -0,0 +1,5 @@
1
+ from .updater import ResyncStateUpdater
2
+
3
+ __all__ = [
4
+ "ResyncStateUpdater",
5
+ ]
@@ -0,0 +1,84 @@
1
+ import datetime
2
+ from typing import Any, Literal
3
+ from port_ocean.clients.port.client import PortClient
4
+ from port_ocean.utils.misc import IntegrationStateStatus
5
+ from port_ocean.utils.time import get_next_occurrence
6
+
7
+
8
+ class ResyncStateUpdater:
9
+ def __init__(self, port_client: PortClient, scheduled_resync_interval: int | None):
10
+ self.port_client = port_client
11
+ self.initiated_at = datetime.datetime.now(tz=datetime.timezone.utc)
12
+ self.scheduled_resync_interval = scheduled_resync_interval
13
+
14
+ # This is used to differ between integration changes that require a full resync and state changes
15
+ # So that the polling event-listener can decide whether to perform a full resync or not
16
+ # TODO: remove this once we separate the state from the integration
17
+ self.last_integration_state_updated_at: str = ""
18
+
19
+ def _calculate_next_scheduled_resync(
20
+ self,
21
+ interval: int | None = None,
22
+ custom_start_time: datetime.datetime | None = None,
23
+ ) -> str | None:
24
+ if interval is None:
25
+ return None
26
+ return get_next_occurrence(
27
+ interval * 60, custom_start_time or self.initiated_at
28
+ ).isoformat()
29
+
30
+ async def update_before_resync(
31
+ self,
32
+ interval: int | None = None,
33
+ custom_start_time: datetime.datetime | None = None,
34
+ ) -> None:
35
+ _interval = interval or self.scheduled_resync_interval
36
+ nest_resync = self._calculate_next_scheduled_resync(
37
+ _interval, custom_start_time
38
+ )
39
+ state: dict[str, Any] = {
40
+ "status": IntegrationStateStatus.Running.value,
41
+ "lastResyncEnd": None,
42
+ "lastResyncStart": datetime.datetime.now(
43
+ tz=datetime.timezone.utc
44
+ ).isoformat(),
45
+ "nextResync": nest_resync,
46
+ "intervalInMinuets": _interval,
47
+ }
48
+
49
+ integration = await self.port_client.update_integration_state(
50
+ state, should_raise=False
51
+ )
52
+ if integration:
53
+ self.last_integration_state_updated_at = integration["resyncState"][
54
+ "updatedAt"
55
+ ]
56
+
57
+ async def update_after_resync(
58
+ self,
59
+ status: Literal[
60
+ IntegrationStateStatus.Completed, IntegrationStateStatus.Failed
61
+ ] = IntegrationStateStatus.Completed,
62
+ interval: int | None = None,
63
+ custom_start_time: datetime.datetime | None = None,
64
+ ) -> None:
65
+ _interval = interval or self.scheduled_resync_interval
66
+ nest_resync = self._calculate_next_scheduled_resync(
67
+ _interval, custom_start_time
68
+ )
69
+ state: dict[str, Any] = {
70
+ "status": status.value,
71
+ "lastResyncEnd": datetime.datetime.now(
72
+ tz=datetime.timezone.utc
73
+ ).isoformat(),
74
+ "nextResync": nest_resync,
75
+ "intervalInMinuets": _interval,
76
+ }
77
+
78
+ integration = await self.port_client.update_integration_state(
79
+ state, should_raise=False
80
+ )
81
+ if integration:
82
+ self.last_integration_state_updated_at = integration["resyncState"][
83
+ "updatedAt"
84
+ ]
port_ocean/core/models.py CHANGED
@@ -1,11 +1,14 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Any, Literal
2
+ from enum import Enum
3
+ from typing import Any
3
4
 
4
5
  from pydantic import BaseModel
5
6
  from pydantic.fields import Field
6
7
 
7
8
 
8
- Runtime = Literal["OnPrem", "Saas"]
9
+ class Runtime(Enum):
10
+ Saas = "Saas"
11
+ OnPrem = "OnPrem"
9
12
 
10
13
 
11
14
  class Entity(BaseModel):
port_ocean/core/utils.py CHANGED
@@ -35,12 +35,13 @@ def is_same_entity(first_entity: Entity, second_entity: Entity) -> bool:
35
35
 
36
36
 
37
37
  async def validate_integration_runtime(
38
- port_client: PortClient, requested_runtime: Runtime
38
+ port_client: PortClient,
39
+ requested_runtime: Runtime,
39
40
  ) -> None:
40
41
  logger.debug("Validating integration runtime")
41
42
  current_integration = await port_client.get_current_integration(should_raise=False)
42
43
  current_runtime = current_integration.get("installationType", "OnPrem")
43
- if current_integration and current_runtime != requested_runtime:
44
+ if current_integration and current_runtime != requested_runtime.value:
44
45
  raise IntegrationRuntimeException(
45
46
  f"Invalid Runtime! Requested to run existing {current_runtime} integration in {requested_runtime} runtime."
46
47
  )
port_ocean/ocean.py CHANGED
@@ -9,6 +9,8 @@ from loguru import logger
9
9
  from pydantic import BaseModel
10
10
  from starlette.types import Scope, Receive, Send
11
11
 
12
+ from port_ocean.core.handlers.resync_state_updater import ResyncStateUpdater
13
+ from port_ocean.core.models import Runtime
12
14
  from port_ocean.clients.port.client import PortClient
13
15
  from port_ocean.config.settings import (
14
16
  IntegrationConfiguration,
@@ -24,6 +26,7 @@ from port_ocean.middlewares import request_handler
24
26
  from port_ocean.utils.repeat import repeat_every
25
27
  from port_ocean.utils.signal import signal_handler
26
28
  from port_ocean.version import __integration_version__
29
+ from port_ocean.utils.misc import IntegrationStateStatus
27
30
 
28
31
 
29
32
  class Ocean:
@@ -63,16 +66,27 @@ class Ocean:
63
66
  integration_class(ocean) if integration_class else BaseIntegration(ocean)
64
67
  )
65
68
 
69
+ self.resync_state_updater = ResyncStateUpdater(
70
+ self.port_client, self.config.scheduled_resync_interval
71
+ )
72
+
73
+ def is_saas(self) -> bool:
74
+ return self.config.runtime == Runtime.Saas
75
+
66
76
  async def _setup_scheduled_resync(
67
77
  self,
68
78
  ) -> None:
69
- def execute_resync_all() -> None:
70
- loop = asyncio.new_event_loop()
71
- asyncio.set_event_loop(loop)
72
-
79
+ async def execute_resync_all() -> None:
80
+ await self.resync_state_updater.update_before_resync()
73
81
  logger.info("Starting a new scheduled resync")
74
- loop.run_until_complete(self.integration.sync_raw_all())
75
- loop.close()
82
+ try:
83
+ await self.integration.sync_raw_all()
84
+ await self.resync_state_updater.update_after_resync()
85
+ except Exception as e:
86
+ await self.resync_state_updater.update_after_resync(
87
+ IntegrationStateStatus.Failed
88
+ )
89
+ raise e
76
90
 
77
91
  interval = self.config.scheduled_resync_interval
78
92
  if interval is not None:
@@ -83,7 +97,11 @@ class Ocean:
83
97
  seconds=interval * 60,
84
98
  # Not running the resync immediately because the event listener should run resync on startup
85
99
  wait_first=True,
86
- )(lambda: threading.Thread(target=execute_resync_all).start())
100
+ )(
101
+ lambda: threading.Thread(
102
+ target=lambda: asyncio.run(execute_resync_all())
103
+ ).start()
104
+ )
87
105
  await repeated_function()
88
106
 
89
107
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
port_ocean/utils/misc.py CHANGED
@@ -1,3 +1,4 @@
1
+ from enum import Enum
1
2
  import inspect
2
3
  from importlib.util import spec_from_file_location, module_from_spec
3
4
  from pathlib import Path
@@ -5,11 +6,16 @@ from time import time
5
6
  from types import ModuleType
6
7
  from typing import Callable, Any
7
8
  from uuid import uuid4
8
-
9
9
  import tomli
10
10
  import yaml
11
11
 
12
12
 
13
+ class IntegrationStateStatus(Enum):
14
+ Running = "running"
15
+ Failed = "failed"
16
+ Completed = "completed"
17
+
18
+
13
19
  def get_time(seconds_precision: bool = True) -> float:
14
20
  """Return current time as Unix/Epoch timestamp, in seconds.
15
21
  :param seconds_precision: if True, return with seconds precision as integer (default).
@@ -0,0 +1,54 @@
1
+ import datetime
2
+ from loguru import logger
3
+
4
+
5
+ def convert_str_to_utc_datetime(time_str: str) -> datetime.datetime | None:
6
+ """
7
+ Convert a string representing time to a datetime object.
8
+ :param time_str: a string representing time in the format "2021-09-01T12:00:00Z"
9
+ """
10
+ aware_date = datetime.datetime.fromisoformat(time_str)
11
+ if time_str.endswith("Z"):
12
+ aware_date = datetime.datetime.fromisoformat(time_str.replace("Z", "+00:00"))
13
+ return aware_date.astimezone(datetime.timezone.utc)
14
+
15
+
16
+ def convert_to_minutes(s: str) -> int:
17
+ minutes_per_unit = {"s": 1 / 60, "m": 1, "h": 60, "d": 1440, "w": 10080}
18
+ try:
19
+ return int(int(s[:-1]) * minutes_per_unit[s[-1]])
20
+ except Exception:
21
+ logger.error(f"Failed converting string to minutes, {s}")
22
+ raise ValueError(
23
+ f"Invalid format. Expected a string ending with {minutes_per_unit.keys()}"
24
+ )
25
+
26
+
27
+ def get_next_occurrence(
28
+ interval_seconds: int,
29
+ start_time: datetime.datetime,
30
+ now: datetime.datetime | None = None,
31
+ ) -> datetime.datetime:
32
+ """
33
+ Predict the next occurrence of an event based on interval, start time, and current time.
34
+
35
+ :param interval_minutes: Interval between occurrences in minutes.
36
+ :param start_time: Start time of the event as a datetime object.
37
+ :param now: Current time as a datetime object.
38
+ :return: The next occurrence time as a datetime object.
39
+ """
40
+
41
+ if now is None:
42
+ now = datetime.datetime.now(tz=datetime.timezone.utc)
43
+ # Calculate the total seconds elapsed since the start time
44
+ elapsed_seconds = (now - start_time).total_seconds()
45
+
46
+ # Calculate the number of intervals that have passed
47
+ intervals_passed = int(elapsed_seconds // interval_seconds)
48
+
49
+ # Calculate the next occurrence time
50
+ next_occurrence = start_time + datetime.timedelta(
51
+ seconds=(intervals_passed + 1) * interval_seconds
52
+ )
53
+
54
+ return next_occurrence
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.9.14
3
+ Version: 0.10.1
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
@@ -9,7 +9,7 @@ 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=y8QFeISoXb31a4RqMhXCDIPa3gJSYz2NPMGEI5p0Hxc,2074
12
+ port_ocean/cli/commands/new.py,sha256=3hefYQrFXJzwfoJgV8yHgnB0HYOVmk3-6XQeb694_Ao,3404
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
@@ -18,18 +18,22 @@ port_ocean/cli/cookiecutter/cookiecutter.json,sha256=N5UrAP2e5JbgEDz_WTQFIZlzSve
18
18
  port_ocean/cli/cookiecutter/extensions.py,sha256=eQNjZvy2enDkJpvMbBGil77Xk9-38f862wfnmCjdoBc,446
19
19
  port_ocean/cli/cookiecutter/hooks/post_gen_project.py,sha256=N-gcNPhVSMGBPI69etVU2QgyhvdzaxNMvUZbyYSdVM4,413
20
20
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore,sha256=9Mz_WI7XBpKzlJ7ILb4vlcuzYkh98Ql3bP_5GHN1sRY,1034
21
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example,sha256=LnNPRe3RnzjWPL4tNLYEQiMvFEZHSy3ceqwQEapcpwE,92
21
22
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore,sha256=32p1lDW_g5hyBz486GWfDeR9m7ikFlASVri5a8vmNoo,2698
22
23
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore,sha256=kCpRPdl3S_jqYYZaOrc0-xa6-l3KqVjNRXc6jCkd_-Q,12
23
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml,sha256=jTYvu-Iayl-bxc917Y7ejcC9KyvH-LSq4-bdYZCYsuM,457
24
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md,sha256=nzAmB0Bjnd2eZo79OjrlyVOdpTBHTmTxvO7c2C8Q-VQ,292
24
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json,sha256=9kf5gY4YjP78vEPfd9j7347sV6wiqeHzmBz7UJkvmDg,1187
25
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml,sha256=pErf4_7-Be2C40JTDJHMywtizUTaQbqyn-SBhCB2ies,507
26
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml,sha256=ie8bI_QOZnJJVG-N1e4KlMebdYts4LUNO_kKw8nGdhA,531
27
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md,sha256=XVSgyxfXJZoZmtwaGbQ8XmCapIthe4E7flfuJub-m_s,338
28
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md,sha256=ZQwD3K35q0wugHZmb1z5wnynmn0uuzwGFSpjm7GieZU,259
25
29
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile,sha256=LsH3vZqqEJkzeQG44cE7JkvPAuh_WPSqYam4YoMvG3M,328
26
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile,sha256=kTa72qEp8pi-joOH6zl8oeIgjEHSCF628p2074yHHNA,1779
30
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile,sha256=2eoJjrQj_zxapvuhX3ohVSgDGgWIF-VwgGvrWPfl5vw,1783
27
31
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md,sha256=5VZmgDRW9gO4d8UuzkujslOIDfIDBiAGL2Hd74HK770,468
28
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore,sha256=kCpRPdl3S_jqYYZaOrc0-xa6-l3KqVjNRXc6jCkd_-Q,12
32
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore,sha256=JAo-DTfS6GthQGP1NH6wLU-ZymwlTea4KHH_jZVTKn0,14
29
33
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/debug.py,sha256=_TRsA2s6GV2E3CTI8CHcsH-ZuH4_Eh5-juDXWaET0ho,65
30
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py,sha256=KxxtBrRygYOP0bulXySVYwX0fm_mm3QHqeEqwDwgXgY,1669
34
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py,sha256=XUwo9yroqSKxAdwVrTKGNGSWvec9n1Rh9Cqvep4HIuE,2257
31
35
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/poetry.toml,sha256=kENq8nNmFPIzw9vySheyG4xHxAPuBSpZO1CYGD6G2NE,46
32
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml,sha256=oR-M7hj8eYit_EzaJeEDSNn4fDCqLJkF3NdKo0L4viM,1999
36
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml,sha256=3DvCdbagz9RczLtBJlmec9zZ5jOepGZ_0SwUE_Xr2ZA,2206
33
37
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/sonar-project.properties,sha256=timmRpoSd50BdxLf45OcFUk2qs855z610kzz3yLAqJw,124
34
38
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
39
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py,sha256=Ew5LA_G1k6DC5a2ygU2FoyjZQa0fRmPy73N0bio0d14,46
@@ -37,10 +41,10 @@ port_ocean/cli/utils.py,sha256=IUK2UbWqjci-lrcDdynZXqVP5B5TcjF0w5CpEVUks-k,54
37
41
  port_ocean/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
42
  port_ocean/clients/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
43
  port_ocean/clients/port/authentication.py,sha256=t3z6h4vld-Tzkpth15sstaMJg0rccX-pXXjNtOa-nCY,2949
40
- port_ocean/clients/port/client.py,sha256=3GYCM0ZkX3pB6sNoOb-7_6dm0Jr5_vqhflD9iltf_As,2640
44
+ port_ocean/clients/port/client.py,sha256=Xd8Jk25Uh4WXY_WW-z1Qbv6F3ZTBFPoOolsxHMfozKw,3366
41
45
  port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
46
  port_ocean/clients/port/mixins/blueprints.py,sha256=8ZVC5i8K1WKQMJJiPqZmgcOlF3OyxWz1aAQ_WA5UW3c,4500
43
- port_ocean/clients/port/mixins/entities.py,sha256=j3YqLb1zMmJrIutYsYsZ_WCT4xh9VXEIH1A0kYMUUtk,8104
47
+ port_ocean/clients/port/mixins/entities.py,sha256=zAyw5FL0rahggdWw-jslTWqQ0ZQgqYitj3CMca8TJPw,8114
44
48
  port_ocean/clients/port/mixins/integrations.py,sha256=Ro6h9BwLxglQWniCVmfC---4oyGuUxk_Qejswl2t-J8,4841
45
49
  port_ocean/clients/port/mixins/migrations.py,sha256=A6896oJF6WbFL2WroyTkMzr12yhVyWqGoq9dtLNSKBY,1457
46
50
  port_ocean/clients/port/retry_transport.py,sha256=PtIZOAZ6V-ncpVysRUsPOgt8Sf01QLnTKB5YeKBxkJk,1861
@@ -49,7 +53,7 @@ port_ocean/clients/port/utils.py,sha256=O9mBu6zp4TfpS4SQ3qCPpn9ZVyYF8GKnji4UnYhM
49
53
  port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
54
  port_ocean/config/base.py,sha256=x1gFbzujrxn7EJudRT81C6eN9WsYAb3vOHwcpcpX8Tc,6370
51
55
  port_ocean/config/dynamic.py,sha256=qOFkRoJsn_BW7581omi_AoMxoHqasf_foxDQ_G11_SI,2030
52
- port_ocean/config/settings.py,sha256=uAch5C-1FrRdWAtwGZvbUROfnmTc83gculpTBfpNAG0,4193
56
+ port_ocean/config/settings.py,sha256=ULv_n7Al94Vyw7Qo4pfbAay6_0sc56s9eAUoOPqVlPo,4274
53
57
  port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
58
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
55
59
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -62,12 +66,12 @@ port_ocean/core/defaults/clean.py,sha256=S3UAfca-oU89WJKIB4OgGjGjPr0vxBQ2aRZsLTZ
62
66
  port_ocean/core/defaults/common.py,sha256=uVUg6VEn4RqtXQwLwMNGfkmT5zYRN_h5USfKw3poVyo,3561
63
67
  port_ocean/core/defaults/initialize.py,sha256=U1JWdpG_snAyqBXEikCjA7JzL7UtpOyx5rLfPhrBIZk,8721
64
68
  port_ocean/core/event_listener/__init__.py,sha256=mzJ33wRq0kh60fpVdOHVmvMTUQIvz3vxmifyBgwDn0E,889
65
- port_ocean/core/event_listener/base.py,sha256=4RujgPz4VfDFlviu4qLGJFnJougSCL-Ewf0rfTQfwgc,1133
69
+ port_ocean/core/event_listener/base.py,sha256=1Nmpg00OfT2AD2L8eFm4VQEcdG2TClpSWJMhWhAjkEE,2356
66
70
  port_ocean/core/event_listener/factory.py,sha256=AYYfSHPAF7P5H-uQECXT0JVJjKDHrYkWJJBSL4mGkg8,3697
67
- port_ocean/core/event_listener/http.py,sha256=UjONC_OODHjpGjvsBPAvO6zGzosdmv5Hx96B4WFqpXs,2577
68
- port_ocean/core/event_listener/kafka.py,sha256=0U_TwmlmtS8N2lprkCmnyOmdqOAvghWxHhSfyu46kEs,6875
69
- port_ocean/core/event_listener/once.py,sha256=KdvUqjIcyp8XqhY1GfR_KYJfParFIRrjCtcMf3JMegk,2150
70
- port_ocean/core/event_listener/polling.py,sha256=3UxjgQJly5y-hA8R798oFWb7bFsYMxSc6GRozA3biiM,3539
71
+ port_ocean/core/event_listener/http.py,sha256=N8HrfFqR3KGKz96pWdp_pP-m30jGtcz_1CkijovkBN8,2565
72
+ port_ocean/core/event_listener/kafka.py,sha256=ulidnp4sz-chXwHsbH9JayVjcxy_mG6ts_Im3YKmLpI,6983
73
+ port_ocean/core/event_listener/once.py,sha256=iL3NkujZOw-7LpxT-EAUJUcAuiAZPm4ZzjHTSt9EdHs,5918
74
+ port_ocean/core/event_listener/polling.py,sha256=d9E3oRLy-Ogb0oadZNxSDgSLIHe4z92uMVwztscZycg,3667
71
75
  port_ocean/core/handlers/__init__.py,sha256=d7ShmS90gLRzGKJA6oNy2Zs_dF2yjkmYZInRhBnO9Rw,572
72
76
  port_ocean/core/handlers/base.py,sha256=cTarblazu8yh8xz2FpB-dzDKuXxtoi143XJgPbV_DcM,157
73
77
  port_ocean/core/handlers/entities_state_applier/__init__.py,sha256=kgLZDCeCEzi4r-0nzW9k78haOZNf6PX7mJOUr34A4c8,173
@@ -83,6 +87,8 @@ port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iE
83
87
  port_ocean/core/handlers/port_app_config/api.py,sha256=6VbKPwFzsWG0IYsVD81hxSmfqtHUFqrfUuj1DBX5g4w,853
84
88
  port_ocean/core/handlers/port_app_config/base.py,sha256=4Nxt2g8voEIHJ4Y1Km5NJcaG2iSbCklw5P8-Kus7Y9k,3007
85
89
  port_ocean/core/handlers/port_app_config/models.py,sha256=4dw6HbgjMG3advpN3x6XF35xsgScnWm0KKTERG4CYZ8,2201
90
+ port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
91
+ port_ocean/core/handlers/resync_state_updater/updater.py,sha256=Yg9ET6ZV5B9GW7u6zZA6GlB_71kmvxvYX2FWgQNzMvo,3182
86
92
  port_ocean/core/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
93
  port_ocean/core/integrations/base.py,sha256=KHsRYFZ38ff3AkkqIQu45883ovgKOJn_fZfnTNy7HWY,2952
88
94
  port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4_RgZnwSRmTCreKcBVM,220
@@ -91,9 +97,9 @@ port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcO
91
97
  port_ocean/core/integrations/mixins/sync.py,sha256=B9fEs8faaYLLikH9GBjE_E61vo0bQDjIGQsQ1SRXOlA,3931
92
98
  port_ocean/core/integrations/mixins/sync_raw.py,sha256=2vyHhGWyPchVfSUKRRfSJ2XsX55ygLWCFrKDaqkNd0o,18386
93
99
  port_ocean/core/integrations/mixins/utils.py,sha256=7y1rGETZIjOQadyIjFJXIHKkQFKx_SwiP-TrAIsyyLY,2303
94
- port_ocean/core/models.py,sha256=4bDvMeydlSpG0YSXjE1Bo2AcZC0aQ2zqFhpsOvZat_A,1190
100
+ port_ocean/core/models.py,sha256=8yYyF4DQ4jMmQJPsmh5-XoF9gfHUTuBit6zuT-bxZ7Y,1228
95
101
  port_ocean/core/ocean_types.py,sha256=3_d8-n626f1kWLQ_Jxw194LEyrOVupz05qs_Y1pvB-A,990
96
- port_ocean/core/utils.py,sha256=WNtU7eqlFzq57PM8b2XVhVYcnjeByBovMuoeG502YGY,3588
102
+ port_ocean/core/utils.py,sha256=GjAm66FiGR48-hT3Mo66G8B4_jYTqSEGh80zs39qYQs,3599
97
103
  port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
104
  port_ocean/exceptions/api.py,sha256=TLmTMqn4uHGaHgZK8PMIJ0TVJlPB4iP7xl9rx7GtCyY,426
99
105
  port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
@@ -110,7 +116,7 @@ port_ocean/log/handlers.py,sha256=k9G_Mb4ga2-Jke9irpdlYqj6EYiwv0gEsh4TgyqqOmI,28
110
116
  port_ocean/log/logger_setup.py,sha256=BaXt-mh9CVXhneh37H46d04lqOdIBixG1pFyGfotuZs,2328
111
117
  port_ocean/log/sensetive.py,sha256=wkyvkKMbyLTjZDSbvvLHL9bv4RvD0DPAyL3uWSttUOA,2916
112
118
  port_ocean/middlewares.py,sha256=6GrhldYAazxSwK2TbS-J28XdZ-9wO3PgCcyIMhnnJvI,2480
113
- port_ocean/ocean.py,sha256=jpWR9D9OVkEcKLky47pkrMBuuNtBZd5XKd8I9xr1gsE,4013
119
+ port_ocean/ocean.py,sha256=0L8sj5TaIJSeYydO7jyANNinB1k0WwN-Q-ChWNnhJOs,4729
114
120
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
121
  port_ocean/run.py,sha256=rTxBlrQd4yyrtgErCFJCHCEHs7d1OXrRiJehUYmIbN0,2212
116
122
  port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
@@ -120,13 +126,14 @@ port_ocean/utils/__init__.py,sha256=KMGnCPXZJbNwtgxtyMycapkDz8tpSyw23MSYT3iVeHs,
120
126
  port_ocean/utils/async_http.py,sha256=arnH458TExn2Dju_Sy6pHas_vF5RMWnOp-jBz5WAAcE,1226
121
127
  port_ocean/utils/async_iterators.py,sha256=buFBiPdsqkNMCk91h6ZG8hJa181j7RjgHajbfgeB8A8,1608
122
128
  port_ocean/utils/cache.py,sha256=3KItZDE2yVrbVDr-hoM8lNna8s2dlpxhP4ICdLjH4LQ,2231
123
- port_ocean/utils/misc.py,sha256=WZjrEDRfyeqbesVt_Nkp2yjazbKF-BOnxJMFAI721yQ,1965
129
+ port_ocean/utils/misc.py,sha256=0q2cJ5psqxn_5u_56pT7vOVQ3shDM02iC1lzyWQ_zl0,2098
124
130
  port_ocean/utils/queue_utils.py,sha256=KWWl8YVnG-glcfIHhM6nefY-2sou_C6DVP1VynQwzB4,2762
125
131
  port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,3231
126
132
  port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
133
+ port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
127
134
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
128
- port_ocean-0.9.14.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
129
- port_ocean-0.9.14.dist-info/METADATA,sha256=iqvytPdN-Ao5wXE955d4-Lnnn_6HKxV8jQ357w_-hmY,6616
130
- port_ocean-0.9.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
131
- port_ocean-0.9.14.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
132
- port_ocean-0.9.14.dist-info/RECORD,,
135
+ port_ocean-0.10.1.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
136
+ port_ocean-0.10.1.dist-info/METADATA,sha256=tNiC4yGVwLDcHumtUn6Xr87FHhYntjVdKvIEvyGLsi8,6616
137
+ port_ocean-0.10.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
138
+ port_ocean-0.10.1.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
139
+ port_ocean-0.10.1.dist-info/RECORD,,