port-ocean 0.15.3__tar.gz → 0.16.1__tar.gz

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 (162) hide show
  1. {port_ocean-0.15.3 → port_ocean-0.16.1}/PKG-INFO +1 -1
  2. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/mixins/entities.py +33 -4
  3. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/context/event.py +11 -0
  4. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/defaults/initialize.py +1 -1
  5. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entities_state_applier/port/applier.py +9 -17
  6. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +5 -2
  7. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entity_processor/jq_entity_processor.py +51 -8
  8. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/mixins/sync_raw.py +21 -4
  9. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/mixins/utils.py +1 -1
  10. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/models.py +4 -0
  11. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/ocean_types.py +11 -2
  12. port_ocean-0.16.1/port_ocean/core/utils/entity_topological_sorter.py +90 -0
  13. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/run.py +1 -1
  14. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +34 -0
  15. port_ocean-0.16.1/port_ocean/tests/core/handlers/mixins/test_sync_raw.py +400 -0
  16. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/core/test_utils.py +1 -1
  17. port_ocean-0.16.1/port_ocean/tests/core/utils/test_entity_topological_sorter.py +99 -0
  18. {port_ocean-0.15.3 → port_ocean-0.16.1}/pyproject.toml +1 -1
  19. {port_ocean-0.15.3 → port_ocean-0.16.1}/LICENSE.md +0 -0
  20. {port_ocean-0.15.3 → port_ocean-0.16.1}/README.md +0 -0
  21. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/Dockerfile.Deb +0 -0
  22. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/Dockerfile.alpine +0 -0
  23. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/Dockerfile.base.builder +0 -0
  24. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/Dockerfile.base.runner +0 -0
  25. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/Dockerfile.dockerignore +0 -0
  26. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/Makefile +0 -0
  27. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/grpcio.sh +0 -0
  28. {port_ocean-0.15.3 → port_ocean-0.16.1}/integrations/_infra/init.sh +0 -0
  29. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/__init__.py +0 -0
  30. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/bootstrap.py +0 -0
  31. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/__init__.py +0 -0
  32. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cli.py +0 -0
  33. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/__init__.py +0 -0
  34. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/defaults/__init___.py +0 -0
  35. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/defaults/clean.py +0 -0
  36. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/defaults/dock.py +0 -0
  37. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/defaults/group.py +0 -0
  38. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/list_integrations.py +0 -0
  39. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/main.py +0 -0
  40. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/new.py +0 -0
  41. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/pull.py +0 -0
  42. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/sail.py +0 -0
  43. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/commands/version.py +0 -0
  44. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/__init__.py +0 -0
  45. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/cookiecutter.json +0 -0
  46. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/extensions.py +0 -0
  47. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/hooks/post_gen_project.py +0 -0
  48. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +0 -0
  49. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore +0 -0
  50. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore +0 -0
  51. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +0 -0
  52. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +0 -0
  53. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +0 -0
  54. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +0 -0
  55. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +0 -0
  56. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md +0 -0
  57. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +0 -0
  58. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/debug.py +0 -0
  59. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +0 -0
  60. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/poetry.toml +0 -0
  61. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +0 -0
  62. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/sonar-project.properties +0 -0
  63. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py +0 -0
  64. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +0 -0
  65. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/cli/utils.py +0 -0
  66. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/__init__.py +0 -0
  67. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/__init__.py +0 -0
  68. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/authentication.py +0 -0
  69. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/client.py +0 -0
  70. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/mixins/__init__.py +0 -0
  71. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/mixins/blueprints.py +0 -0
  72. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/mixins/integrations.py +0 -0
  73. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/mixins/migrations.py +0 -0
  74. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/retry_transport.py +0 -0
  75. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/types.py +0 -0
  76. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/clients/port/utils.py +0 -0
  77. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/config/__init__.py +0 -0
  78. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/config/base.py +0 -0
  79. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/config/dynamic.py +0 -0
  80. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/config/settings.py +0 -0
  81. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/consumers/__init__.py +0 -0
  82. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/consumers/kafka_consumer.py +0 -0
  83. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/context/__init__.py +0 -0
  84. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/context/ocean.py +0 -0
  85. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/context/resource.py +0 -0
  86. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/__init__.py +0 -0
  87. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/defaults/__init__.py +0 -0
  88. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/defaults/clean.py +0 -0
  89. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/defaults/common.py +0 -0
  90. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/event_listener/__init__.py +0 -0
  91. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/event_listener/base.py +0 -0
  92. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/event_listener/factory.py +0 -0
  93. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/event_listener/http.py +0 -0
  94. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/event_listener/kafka.py +0 -0
  95. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/event_listener/once.py +0 -0
  96. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/event_listener/polling.py +0 -0
  97. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/__init__.py +0 -0
  98. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/base.py +0 -0
  99. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entities_state_applier/__init__.py +0 -0
  100. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entities_state_applier/base.py +0 -0
  101. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entities_state_applier/port/__init__.py +0 -0
  102. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py +0 -0
  103. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entity_processor/__init__.py +0 -0
  104. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/entity_processor/base.py +0 -0
  105. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/port_app_config/__init__.py +0 -0
  106. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/port_app_config/api.py +0 -0
  107. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/port_app_config/base.py +0 -0
  108. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/port_app_config/models.py +0 -0
  109. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/resync_state_updater/__init__.py +0 -0
  110. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/handlers/resync_state_updater/updater.py +0 -0
  111. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/__init__.py +0 -0
  112. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/base.py +0 -0
  113. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/mixins/__init__.py +0 -0
  114. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/mixins/events.py +0 -0
  115. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/mixins/handler.py +0 -0
  116. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/core/integrations/mixins/sync.py +0 -0
  117. {port_ocean-0.15.3/port_ocean/core → port_ocean-0.16.1/port_ocean/core/utils}/utils.py +0 -0
  118. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/debug_cli.py +0 -0
  119. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/__init__.py +0 -0
  120. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/api.py +0 -0
  121. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/base.py +0 -0
  122. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/clients.py +0 -0
  123. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/context.py +0 -0
  124. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/core.py +0 -0
  125. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/port_defaults.py +0 -0
  126. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/exceptions/utils.py +0 -0
  127. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/helpers/__init__.py +0 -0
  128. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/helpers/async_client.py +0 -0
  129. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/helpers/retry.py +0 -0
  130. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/log/__init__.py +0 -0
  131. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/log/handlers.py +0 -0
  132. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/log/logger_setup.py +0 -0
  133. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/log/sensetive.py +0 -0
  134. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/middlewares.py +0 -0
  135. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/ocean.py +0 -0
  136. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/py.typed +0 -0
  137. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/sonar-project.properties +0 -0
  138. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/__init__.py +0 -0
  139. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/clients/port/mixins/test_entities.py +0 -0
  140. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/conftest.py +0 -0
  141. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/core/defaults/test_common.py +0 -0
  142. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/helpers/__init__.py +0 -0
  143. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/helpers/fake_port_api.py +0 -0
  144. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/helpers/fixtures.py +0 -0
  145. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/helpers/integration.py +0 -0
  146. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/helpers/ocean_app.py +0 -0
  147. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/helpers/port_client.py +0 -0
  148. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/helpers/smoke_test.py +0 -0
  149. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/log/test_handlers.py +0 -0
  150. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/test_smoke.py +0 -0
  151. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/utils/test_async_iterators.py +0 -0
  152. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/tests/utils/test_cache.py +0 -0
  153. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/__init__.py +0 -0
  154. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/async_http.py +0 -0
  155. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/async_iterators.py +0 -0
  156. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/cache.py +0 -0
  157. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/misc.py +0 -0
  158. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/queue_utils.py +0 -0
  159. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/repeat.py +0 -0
  160. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/signal.py +0 -0
  161. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/utils/time.py +0 -0
  162. {port_ocean-0.15.3 → port_ocean-0.16.1}/port_ocean/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.15.3
3
+ Version: 0.16.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
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import Any
2
+ from typing import Any, Literal
3
3
  from urllib.parse import quote_plus
4
4
 
5
5
  import httpx
@@ -11,7 +11,8 @@ from port_ocean.clients.port.utils import (
11
11
  handle_status_code,
12
12
  PORT_HTTP_MAX_CONNECTIONS_LIMIT,
13
13
  )
14
- from port_ocean.core.models import Entity
14
+ from port_ocean.core.models import Entity, PortAPIErrorMessage
15
+ from starlette import status
15
16
 
16
17
 
17
18
  class EntityClientMixin:
@@ -29,7 +30,27 @@ class EntityClientMixin:
29
30
  request_options: RequestOptions,
30
31
  user_agent_type: UserAgentType | None = None,
31
32
  should_raise: bool = True,
32
- ) -> Entity | None:
33
+ ) -> Entity | None | Literal[False]:
34
+ """
35
+ This function upserts an entity into Port.
36
+
37
+ Usage:
38
+ ```python
39
+ upsertedEntity = await self.context.port_client.upsert_entity(
40
+ entity,
41
+ event.port_app_config.get_port_request_options(),
42
+ user_agent_type,
43
+ should_raise=False,
44
+ )
45
+ ```
46
+ :param entity: An Entity to be upserted
47
+ :param request_options: A dictionary specifying how to upsert the entity
48
+ :param user_agent_type: a UserAgentType specifying who is preforming the action
49
+ :param should_raise: A boolean specifying whether the error should be raised or handled silently
50
+ :return: [Entity] if the upsert occured successfully
51
+ :return: [None] will be returned if entity is using search identifier
52
+ :return: [False] will be returned if upsert failed because of unmet dependency
53
+ """
33
54
  validation_only = request_options["validation_only"]
34
55
  async with self.semaphore:
35
56
  logger.debug(
@@ -50,13 +71,21 @@ class EntityClientMixin:
50
71
  },
51
72
  extensions={"retryable": True},
52
73
  )
53
-
54
74
  if response.is_error:
55
75
  logger.error(
56
76
  f"Error {'Validating' if validation_only else 'Upserting'} "
57
77
  f"entity: {entity.identifier} of "
58
78
  f"blueprint: {entity.blueprint}"
59
79
  )
80
+ result = response.json()
81
+
82
+ if (
83
+ response.status_code == status.HTTP_404_NOT_FOUND
84
+ and not result.get("ok")
85
+ and result.get("error") == PortAPIErrorMessage.NOT_FOUND.value
86
+ ):
87
+ # Return false to differentiate from `result_entity.is_using_search_identifier`
88
+ return False
60
89
  handle_status_code(response, should_raise)
61
90
  result = response.json()
62
91
 
@@ -14,6 +14,7 @@ from typing import (
14
14
  from uuid import uuid4
15
15
 
16
16
  from loguru import logger
17
+ from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
17
18
  from pydispatch import dispatcher # type: ignore
18
19
  from werkzeug.local import LocalStack, LocalProxy
19
20
 
@@ -24,6 +25,7 @@ from port_ocean.exceptions.context import (
24
25
  )
25
26
  from port_ocean.utils.misc import get_time
26
27
 
28
+
27
29
  if TYPE_CHECKING:
28
30
  from port_ocean.core.handlers.port_app_config.models import (
29
31
  ResourceConfig,
@@ -50,6 +52,9 @@ class EventContext:
50
52
  _parent_event: Optional["EventContext"] = None
51
53
  _event_id: str = field(default_factory=lambda: str(uuid4()))
52
54
  _on_abort_callbacks: list[AbortCallbackFunction] = field(default_factory=list)
55
+ entity_topological_sorter: EntityTopologicalSorter = field(
56
+ default_factory=EntityTopologicalSorter
57
+ )
53
58
 
54
59
  def on_abort(self, func: AbortCallbackFunction) -> None:
55
60
  self._on_abort_callbacks.append(func)
@@ -129,6 +134,11 @@ async def event_context(
129
134
  ) -> AsyncIterator[EventContext]:
130
135
  parent = parent_override or _event_context_stack.top
131
136
  parent_attributes = parent.attributes if parent else {}
137
+ entity_topological_sorter = (
138
+ parent.entity_topological_sorter
139
+ if parent and parent.entity_topological_sorter
140
+ else EntityTopologicalSorter()
141
+ )
132
142
 
133
143
  attributes = {**parent_attributes, **(attributes or {})}
134
144
  new_event = EventContext(
@@ -138,6 +148,7 @@ async def event_context(
138
148
  _parent_event=parent,
139
149
  # inherit port app config from parent event, so it can be used in nested events
140
150
  _port_app_config=parent.port_app_config if parent else None,
151
+ entity_topological_sorter=entity_topological_sorter,
141
152
  )
142
153
  _event_context_stack.push(new_event)
143
154
 
@@ -14,7 +14,7 @@ from port_ocean.core.defaults.common import (
14
14
  )
15
15
  from port_ocean.core.handlers.port_app_config.models import PortAppConfig
16
16
  from port_ocean.core.models import Blueprint
17
- from port_ocean.core.utils import gather_and_split_errors_from_results
17
+ from port_ocean.core.utils.utils import gather_and_split_errors_from_results
18
18
  from port_ocean.exceptions.port_defaults import (
19
19
  AbortDefaultCreationError,
20
20
  )
@@ -8,12 +8,11 @@ from port_ocean.core.handlers.entities_state_applier.base import (
8
8
  from port_ocean.core.handlers.entities_state_applier.port.get_related_entities import (
9
9
  get_related_entities,
10
10
  )
11
- from port_ocean.core.handlers.entities_state_applier.port.order_by_entities_dependencies import (
12
- order_by_entities_dependencies,
13
- )
11
+
14
12
  from port_ocean.core.models import Entity
15
13
  from port_ocean.core.ocean_types import EntityDiff
16
- from port_ocean.core.utils import is_same_entity, get_port_diff
14
+ from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
15
+ from port_ocean.core.utils.utils import is_same_entity, get_port_diff
17
16
 
18
17
 
19
18
  class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
@@ -106,19 +105,7 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
106
105
  should_raise=False,
107
106
  )
108
107
  else:
109
- entities_with_search_identifier: list[Entity] = []
110
- entities_without_search_identifier: list[Entity] = []
111
108
  for entity in entities:
112
- if entity.is_using_search_identifier:
113
- entities_with_search_identifier.append(entity)
114
- else:
115
- entities_without_search_identifier.append(entity)
116
-
117
- ordered_created_entities = reversed(
118
- entities_with_search_identifier
119
- + order_by_entities_dependencies(entities_without_search_identifier)
120
- )
121
- for entity in ordered_created_entities:
122
109
  upsertedEntity = await self.context.port_client.upsert_entity(
123
110
  entity,
124
111
  event.port_app_config.get_port_request_options(),
@@ -127,6 +114,9 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
127
114
  )
128
115
  if upsertedEntity:
129
116
  modified_entities.append(upsertedEntity)
117
+ # condition to false to differentiate from `result_entity.is_using_search_identifier`
118
+ if upsertedEntity is False:
119
+ event.entity_topological_sorter.register_entity(entity)
130
120
  return modified_entities
131
121
 
132
122
  async def delete(
@@ -141,7 +131,9 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
141
131
  should_raise=False,
142
132
  )
143
133
  else:
144
- ordered_deleted_entities = order_by_entities_dependencies(entities)
134
+ ordered_deleted_entities = (
135
+ EntityTopologicalSorter.order_by_entities_dependencies(entities)
136
+ )
145
137
 
146
138
  for entity in ordered_deleted_entities:
147
139
  await self.context.port_client.delete_entity(
@@ -14,7 +14,6 @@ def node(entity: Entity) -> Node:
14
14
  def order_by_entities_dependencies(entities: list[Entity]) -> list[Entity]:
15
15
  nodes: dict[Node, Set[Node]] = {}
16
16
  entities_map = {}
17
-
18
17
  for entity in entities:
19
18
  nodes[node(entity)] = set()
20
19
  entities_map[node(entity)] = entity
@@ -33,7 +32,11 @@ def order_by_entities_dependencies(entities: list[Entity]) -> list[Entity]:
33
32
  ]
34
33
 
35
34
  for related_entity in related_entities:
36
- nodes[node(entity)].add(node(related_entity))
35
+ if (
36
+ entity.blueprint is not related_entity.blueprint
37
+ or entity.identifier is not related_entity.identifier
38
+ ):
39
+ nodes[node(entity)].add(node(related_entity))
37
40
 
38
41
  sort_op = TopologicalSorter(nodes)
39
42
  try:
@@ -16,7 +16,10 @@ from port_ocean.core.ocean_types import (
16
16
  EntitySelectorDiff,
17
17
  CalculationResult,
18
18
  )
19
- from port_ocean.core.utils import gather_and_split_errors_from_results, zip_and_sum
19
+ from port_ocean.core.utils.utils import (
20
+ gather_and_split_errors_from_results,
21
+ zip_and_sum,
22
+ )
20
23
  from port_ocean.exceptions.core import EntityProcessorException
21
24
  from port_ocean.utils.queue_utils import process_in_queue
22
25
 
@@ -31,6 +34,7 @@ class MappedEntity:
31
34
  entity: dict[str, Any] = field(default_factory=dict)
32
35
  did_entity_pass_selector: bool = False
33
36
  raw_data: Optional[dict[str, Any]] = None
37
+ misconfigurations: dict[str, str] = field(default_factory=dict)
34
38
 
35
39
 
36
40
  class JQEntityProcessor(BaseEntityProcessor):
@@ -92,21 +96,37 @@ class JQEntityProcessor(BaseEntityProcessor):
92
96
  )
93
97
 
94
98
  async def _search_as_object(
95
- self, data: dict[str, Any], obj: dict[str, Any]
99
+ self,
100
+ data: dict[str, Any],
101
+ obj: dict[str, Any],
102
+ misconfigurations: dict[str, str] | None = None,
96
103
  ) -> dict[str, Any | None]:
104
+ """
105
+ Identify and extract the relevant value for the chosen key and populate it into the entity
106
+ :param data: the property itself that holds the key and the value, it is being passed to the task and we get back a task item,
107
+ if the data is a dict, we will recursively call this function again.
108
+ :param obj: the key that we want its value to be mapped into our entity.
109
+ :param misconfigurations: due to the recursive nature of this function,
110
+ we aim to have a dict that represents all of the misconfigured properties and when used recursively,
111
+ we pass this reference to misfoncigured object to add the relevant misconfigured keys.
112
+ :return: Mapped object with found value.
113
+ """
114
+
97
115
  search_tasks: dict[
98
116
  str, Task[dict[str, Any | None]] | list[Task[dict[str, Any | None]]]
99
117
  ] = {}
100
118
  for key, value in obj.items():
101
119
  if isinstance(value, list):
102
120
  search_tasks[key] = [
103
- asyncio.create_task(self._search_as_object(data, obj))
121
+ asyncio.create_task(
122
+ self._search_as_object(data, obj, misconfigurations)
123
+ )
104
124
  for obj in value
105
125
  ]
106
126
 
107
127
  elif isinstance(value, dict):
108
128
  search_tasks[key] = asyncio.create_task(
109
- self._search_as_object(data, value)
129
+ self._search_as_object(data, value, misconfigurations)
110
130
  )
111
131
  else:
112
132
  search_tasks[key] = asyncio.create_task(self._search(data, value))
@@ -115,12 +135,20 @@ class JQEntityProcessor(BaseEntityProcessor):
115
135
  for key, task in search_tasks.items():
116
136
  try:
117
137
  if isinstance(task, list):
118
- result[key] = [await task for task in task]
138
+ result_list = []
139
+ for task in task:
140
+ task_result = await task
141
+ if task_result is None and misconfigurations is not None:
142
+ misconfigurations[key] = obj[key]
143
+ result_list.append(task_result)
144
+ result[key] = result_list
119
145
  else:
120
- result[key] = await task
146
+ task_result = await task
147
+ if task_result is None and misconfigurations is not None:
148
+ misconfigurations[key] = obj[key]
149
+ result[key] = task_result
121
150
  except Exception:
122
151
  result[key] = None
123
-
124
152
  return result
125
153
 
126
154
  async def _get_mapped_entity(
@@ -132,11 +160,15 @@ class JQEntityProcessor(BaseEntityProcessor):
132
160
  ) -> MappedEntity:
133
161
  should_run = await self._search_as_bool(data, selector_query)
134
162
  if parse_all or should_run:
135
- mapped_entity = await self._search_as_object(data, raw_entity_mappings)
163
+ misconfigurations: dict[str, str] = {}
164
+ mapped_entity = await self._search_as_object(
165
+ data, raw_entity_mappings, misconfigurations
166
+ )
136
167
  return MappedEntity(
137
168
  mapped_entity,
138
169
  did_entity_pass_selector=should_run,
139
170
  raw_data=data if should_run else None,
171
+ misconfigurations=misconfigurations,
140
172
  )
141
173
 
142
174
  return MappedEntity()
@@ -218,7 +250,11 @@ class JQEntityProcessor(BaseEntityProcessor):
218
250
  passed_entities = []
219
251
  failed_entities = []
220
252
  examples_to_send: list[dict[str, Any]] = []
253
+ entity_misconfigurations: dict[str, str] = {}
254
+ missing_required_fields: bool = False
221
255
  for result in calculated_entities_results:
256
+ if len(result.misconfigurations) > 0:
257
+ entity_misconfigurations |= result.misconfigurations
222
258
  if result.entity.get("identifier") and result.entity.get("blueprint"):
223
259
  parsed_entity = Entity.parse_obj(result.entity)
224
260
  if result.did_entity_pass_selector:
@@ -230,6 +266,12 @@ class JQEntityProcessor(BaseEntityProcessor):
230
266
  examples_to_send.append(result.raw_data)
231
267
  else:
232
268
  failed_entities.append(parsed_entity)
269
+ else:
270
+ missing_required_fields = True
271
+ if len(entity_misconfigurations) > 0:
272
+ logger.info(
273
+ f"The mapping resulted with invalid values for{" identifier, blueprint," if missing_required_fields else " "} properties. Mapping result: {entity_misconfigurations}"
274
+ )
233
275
  if (
234
276
  not calculated_entities_results
235
277
  and raw_results
@@ -245,4 +287,5 @@ class JQEntityProcessor(BaseEntityProcessor):
245
287
  return CalculationResult(
246
288
  EntitySelectorDiff(passed=passed_entities, failed=failed_entities),
247
289
  errors,
290
+ misonfigured_entity_keys=entity_misconfigurations,
248
291
  )
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from graphlib import CycleError
2
3
  import inspect
3
4
  import typing
4
5
  from typing import Callable, Awaitable, Any
@@ -27,7 +28,7 @@ from port_ocean.core.ocean_types import (
27
28
  RAW_ITEM,
28
29
  CalculationResult,
29
30
  )
30
- from port_ocean.core.utils import zip_and_sum, gather_and_split_errors_from_results
31
+ from port_ocean.core.utils.utils import zip_and_sum, gather_and_split_errors_from_results
31
32
  from port_ocean.exceptions.core import OceanAbortException
32
33
 
33
34
  SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
@@ -184,7 +185,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
184
185
  send_raw_data_examples_amount = (
185
186
  SEND_RAW_DATA_EXAMPLES_AMOUNT if ocean.config.send_raw_data_examples else 0
186
187
  )
187
- all_entities, register_errors = await self._register_resource_raw(
188
+ all_entities, register_errors,_ = await self._register_resource_raw(
188
189
  resource_config,
189
190
  raw_results,
190
191
  user_agent_type,
@@ -201,7 +202,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
201
202
  0, send_raw_data_examples_amount - len(passed_entities)
202
203
  )
203
204
 
204
- entities, register_errors = await self._register_resource_raw(
205
+ entities, register_errors,_ = await self._register_resource_raw(
205
206
  resource_config,
206
207
  items,
207
208
  user_agent_type,
@@ -396,7 +397,20 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
396
397
  {"before": entities_before_flatten, "after": entities_after_flatten},
397
398
  user_agent_type,
398
399
  )
399
-
400
+ async def sort_and_upsert_failed_entities(self,user_agent_type: UserAgentType)->None:
401
+ try:
402
+ if not event.entity_topological_sorter.should_execute():
403
+ return None
404
+ logger.info(f"Executings topological sort of {event.entity_topological_sorter.get_entities_count()} entities failed to upsert.",failed_toupsert_entities_count=event.entity_topological_sorter.get_entities_count())
405
+
406
+ for entity in event.entity_topological_sorter.get_entities():
407
+ await self.entities_state_applier.context.port_client.upsert_entity(entity,event.port_app_config.get_port_request_options(),user_agent_type,should_raise=False)
408
+
409
+ except OceanAbortException as ocean_abort:
410
+ logger.info(f"Failed topological sort of failed to upsert entites - trying to upsert unordered {event.entity_topological_sorter.get_entities_count()} entities.",failed_topological_sort_entities_count=event.entity_topological_sorter.get_entities_count() )
411
+ if isinstance(ocean_abort.__cause__,CycleError):
412
+ for entity in event.entity_topological_sorter.get_entities(False):
413
+ await self.entities_state_applier.context.port_client.upsert_entity(entity,event.port_app_config.get_port_request_options(),user_agent_type,should_raise=False)
400
414
  async def sync_raw_all(
401
415
  self,
402
416
  _: dict[Any, Any] | None = None,
@@ -426,6 +440,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
426
440
  use_cache=False
427
441
  )
428
442
  logger.info(f"Resync will use the following mappings: {app_config.dict()}")
443
+
429
444
  try:
430
445
  did_fetched_current_state = True
431
446
  entities_at_port = await ocean.port_client.search_entities(
@@ -455,6 +470,8 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
455
470
  event.on_abort(lambda: task.cancel())
456
471
 
457
472
  creation_results.append(await task)
473
+
474
+ await self.sort_and_upsert_failed_entities(user_agent_type)
458
475
  except asyncio.CancelledError as e:
459
476
  logger.warning("Resync aborted successfully, skipping delete phase. This leads to an incomplete state")
460
477
  raise
@@ -9,7 +9,7 @@ from port_ocean.core.ocean_types import (
9
9
  RESYNC_EVENT_LISTENER,
10
10
  RESYNC_RESULT,
11
11
  )
12
- from port_ocean.core.utils import validate_result
12
+ from port_ocean.core.utils.utils import validate_result
13
13
  from port_ocean.exceptions.core import (
14
14
  RawObjectValidationException,
15
15
  OceanAbortException,
@@ -27,6 +27,10 @@ class Runtime(Enum):
27
27
  ) or installation_type == self.value
28
28
 
29
29
 
30
+ class PortAPIErrorMessage(Enum):
31
+ NOT_FOUND = "not_found"
32
+
33
+
30
34
  class Entity(BaseModel):
31
35
  identifier: Any
32
36
  blueprint: Any
@@ -1,5 +1,13 @@
1
- from typing import TypedDict, Any, AsyncIterator, Callable, Awaitable, NamedTuple
2
-
1
+ from typing import (
2
+ TypedDict,
3
+ Any,
4
+ AsyncIterator,
5
+ Callable,
6
+ Awaitable,
7
+ NamedTuple,
8
+ )
9
+
10
+ from dataclasses import field
3
11
  from port_ocean.core.models import Entity
4
12
 
5
13
  RAW_ITEM = dict[Any, Any]
@@ -30,6 +38,7 @@ class EntitySelectorDiff(NamedTuple):
30
38
  class CalculationResult(NamedTuple):
31
39
  entity_selector_diff: EntitySelectorDiff
32
40
  errors: list[Exception]
41
+ misonfigured_entity_keys: dict[str, str] = field(default_factory=dict)
33
42
 
34
43
 
35
44
  class IntegrationEventsCallbacks(TypedDict):
@@ -0,0 +1,90 @@
1
+ from typing import Any, Generator
2
+ from port_ocean.context import event
3
+ from port_ocean.core.models import Entity
4
+
5
+ from loguru import logger
6
+
7
+ from graphlib import TopologicalSorter, CycleError
8
+ from typing import Set
9
+
10
+ from port_ocean.exceptions.core import OceanAbortException
11
+
12
+ Node = tuple[str, str]
13
+
14
+
15
+ class EntityTopologicalSorter:
16
+ def __init__(self) -> None:
17
+ self.entities: list[Entity] = []
18
+
19
+ def register_entity(
20
+ self,
21
+ entity: Entity,
22
+ ) -> None:
23
+ logger.debug(
24
+ f"Will retry upserting entity - {entity.identifier} at the end of resync"
25
+ )
26
+ self.entities.append(entity)
27
+
28
+ def should_execute(self) -> int:
29
+ return not event.event.port_app_config.create_missing_related_entities
30
+
31
+ def get_entities_count(self) -> int:
32
+ return len(self.entities)
33
+
34
+ def get_entities(self, sorted: bool = True) -> Generator[Entity, Any, None]:
35
+ if not sorted:
36
+ for entity in self.entities:
37
+ yield entity
38
+ return
39
+
40
+ sorted_and_mapped = EntityTopologicalSorter.order_by_entities_dependencies(
41
+ self.entities
42
+ )
43
+ for entity in sorted_and_mapped:
44
+ yield entity
45
+
46
+ @staticmethod
47
+ def node(entity: Entity) -> Node:
48
+ return entity.identifier, entity.blueprint
49
+
50
+ @staticmethod
51
+ def order_by_entities_dependencies(entities: list[Entity]) -> list[Entity]:
52
+ nodes: dict[Node, Set[Node]] = {}
53
+ entities_map = {}
54
+ for entity in entities:
55
+ nodes[EntityTopologicalSorter.node(entity)] = set()
56
+ entities_map[EntityTopologicalSorter.node(entity)] = entity
57
+
58
+ for entity in entities:
59
+ relation_target_ids: list[str] = sum(
60
+ [
61
+ identifiers if isinstance(identifiers, list) else [identifiers]
62
+ for identifiers in entity.relations.values()
63
+ if identifiers is not None
64
+ ],
65
+ [],
66
+ )
67
+ related_entities = [
68
+ related
69
+ for related in entities
70
+ if related.identifier in relation_target_ids
71
+ ]
72
+
73
+ for related_entity in related_entities:
74
+ if (
75
+ entity.blueprint is not related_entity.blueprint
76
+ or entity.identifier is not related_entity.identifier
77
+ ):
78
+ nodes[EntityTopologicalSorter.node(entity)].add(
79
+ EntityTopologicalSorter.node(related_entity)
80
+ )
81
+
82
+ sort_op = TopologicalSorter(nodes)
83
+ try:
84
+ return [entities_map[item] for item in sort_op.static_order()]
85
+ except CycleError as ex:
86
+ raise OceanAbortException(
87
+ "Cannot order entities due to cyclic dependencies. \n"
88
+ "If you do want to have cyclic dependencies, please make sure to set the keys"
89
+ " 'createMissingRelatedEntities' and 'deleteDependentEntities' in the integration config in Port."
90
+ ) from ex
@@ -9,7 +9,7 @@ from port_ocean.bootstrap import create_default_app
9
9
  from port_ocean.config.dynamic import default_config_factory
10
10
  from port_ocean.config.settings import ApplicationSettings, LogLevelType
11
11
  from port_ocean.core.defaults.initialize import initialize_defaults
12
- from port_ocean.core.utils import validate_integration_runtime
12
+ from port_ocean.core.utils.utils import validate_integration_runtime
13
13
  from port_ocean.log.logger_setup import setup_logger
14
14
  from port_ocean.ocean import Ocean
15
15
  from port_ocean.utils.misc import get_spec_file, load_module
@@ -269,3 +269,37 @@ class TestJQEntityProcessor:
269
269
  assert len(result.entity_selector_diff.passed) == 1
270
270
  assert result.entity_selector_diff.passed[0].properties.get("foo") == "bar"
271
271
  assert not result.errors
272
+
273
+ async def test_parse_items_wrong_mapping(
274
+ self, mocked_processor: JQEntityProcessor
275
+ ) -> None:
276
+ mapping = Mock()
277
+ mapping.port.entity.mappings.dict.return_value = {
278
+ "title": ".foo",
279
+ "identifier": ".ark",
280
+ "blueprint": ".baz",
281
+ "properties": {
282
+ "description": ".bazbar",
283
+ "url": ".foobar",
284
+ "defaultBranch": ".bar.baz",
285
+ },
286
+ }
287
+ mapping.port.items_to_parse = None
288
+ mapping.selector.query = "true"
289
+ raw_results = [
290
+ {
291
+ "foo": "bar",
292
+ "baz": "bazbar",
293
+ "bar": {"foobar": "barfoo", "baz": "barbaz"},
294
+ },
295
+ {"foo": "bar", "baz": "bazbar", "bar": {"foobar": "foobar"}},
296
+ ]
297
+ result = await mocked_processor._parse_items(mapping, raw_results)
298
+ assert len(result.misonfigured_entity_keys) > 0
299
+ assert len(result.misonfigured_entity_keys) == 4
300
+ assert result.misonfigured_entity_keys == {
301
+ "identifier": ".ark",
302
+ "description": ".bazbar",
303
+ "url": ".foobar",
304
+ "defaultBranch": ".bar.baz",
305
+ }