port-ocean 0.5.10__tar.gz → 0.5.13__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 (129) hide show
  1. {port_ocean-0.5.10 → port_ocean-0.5.13}/PKG-INFO +4 -3
  2. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/authentication.py +3 -1
  3. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/mixins/entities.py +33 -50
  4. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/retry_transport.py +0 -5
  5. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entities_state_applier/port/applier.py +13 -75
  6. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entity_processor/base.py +17 -7
  7. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entity_processor/jq_entity_processor.py +42 -41
  8. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/mixins/sync_raw.py +56 -41
  9. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/ocean_types.py +10 -2
  10. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/helpers/retry.py +49 -4
  11. port_ocean-0.5.13/port_ocean/utils/async_iterators.py +49 -0
  12. {port_ocean-0.5.10 → port_ocean-0.5.13}/pyproject.toml +5 -4
  13. port_ocean-0.5.10/port_ocean/core/handlers/entities_state_applier/port/validate_entity_relations.py +0 -40
  14. {port_ocean-0.5.10 → port_ocean-0.5.13}/LICENSE.md +0 -0
  15. {port_ocean-0.5.10 → port_ocean-0.5.13}/README.md +0 -0
  16. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/__init__.py +0 -0
  17. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/bootstrap.py +0 -0
  18. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/__init__.py +0 -0
  19. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cli.py +0 -0
  20. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/__init__.py +0 -0
  21. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/defaults/__init___.py +0 -0
  22. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/defaults/clean.py +0 -0
  23. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/defaults/dock.py +0 -0
  24. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/defaults/group.py +0 -0
  25. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/list_integrations.py +0 -0
  26. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/main.py +0 -0
  27. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/new.py +0 -0
  28. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/pull.py +0 -0
  29. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/sail.py +0 -0
  30. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/commands/version.py +0 -0
  31. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/__init__.py +0 -0
  32. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/cookiecutter.json +0 -0
  33. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/extensions.py +0 -0
  34. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/hooks/post_gen_project.py +0 -0
  35. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -0
  36. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore +0 -0
  37. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore +0 -0
  38. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +0 -0
  39. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +0 -0
  40. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -0
  41. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile +0 -0
  42. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md +0 -0
  43. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +0 -0
  44. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -0
  45. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/debug.py +0 -0
  46. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +0 -0
  47. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/poetry.toml +0 -0
  48. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +0 -0
  49. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/sonar-project.properties +0 -0
  50. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py +0 -0
  51. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/cli/utils.py +0 -0
  52. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/__init__.py +0 -0
  53. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/__init__.py +0 -0
  54. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/client.py +0 -0
  55. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/mixins/__init__.py +0 -0
  56. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/mixins/blueprints.py +0 -0
  57. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/mixins/integrations.py +0 -0
  58. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/mixins/migrations.py +0 -0
  59. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/types.py +0 -0
  60. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/clients/port/utils.py +0 -0
  61. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/config/__init__.py +0 -0
  62. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/config/base.py +0 -0
  63. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/config/dynamic.py +0 -0
  64. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/config/settings.py +0 -0
  65. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/consumers/__init__.py +0 -0
  66. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/consumers/kafka_consumer.py +0 -0
  67. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/context/__init__.py +0 -0
  68. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/context/event.py +0 -0
  69. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/context/ocean.py +0 -0
  70. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/context/resource.py +0 -0
  71. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/__init__.py +0 -0
  72. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/defaults/__init__.py +0 -0
  73. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/defaults/clean.py +0 -0
  74. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/defaults/common.py +0 -0
  75. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/defaults/initialize.py +0 -0
  76. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/event_listener/__init__.py +0 -0
  77. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/event_listener/base.py +0 -0
  78. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/event_listener/factory.py +0 -0
  79. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/event_listener/http.py +0 -0
  80. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/event_listener/kafka.py +0 -0
  81. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/event_listener/once.py +0 -0
  82. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/event_listener/polling.py +0 -0
  83. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/__init__.py +0 -0
  84. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/base.py +0 -0
  85. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entities_state_applier/__init__.py +0 -0
  86. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entities_state_applier/base.py +0 -0
  87. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entities_state_applier/port/__init__.py +0 -0
  88. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py +0 -0
  89. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +0 -0
  90. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/entity_processor/__init__.py +0 -0
  91. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/port_app_config/__init__.py +0 -0
  92. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/port_app_config/api.py +0 -0
  93. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/port_app_config/base.py +0 -0
  94. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/handlers/port_app_config/models.py +0 -0
  95. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/__init__.py +0 -0
  96. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/base.py +0 -0
  97. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/mixins/__init__.py +0 -0
  98. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/mixins/events.py +0 -0
  99. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/mixins/handler.py +0 -0
  100. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/mixins/sync.py +0 -0
  101. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/integrations/mixins/utils.py +0 -0
  102. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/models.py +0 -0
  103. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/core/utils.py +0 -0
  104. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/__init__.py +0 -0
  105. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/api.py +0 -0
  106. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/base.py +0 -0
  107. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/clients.py +0 -0
  108. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/context.py +0 -0
  109. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/core.py +0 -0
  110. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/port_defaults.py +0 -0
  111. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/exceptions/utils.py +0 -0
  112. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/helpers/__init__.py +0 -0
  113. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/helpers/async_client.py +0 -0
  114. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/log/__init__.py +0 -0
  115. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/log/handlers.py +0 -0
  116. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/log/logger_setup.py +0 -0
  117. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/log/sensetive.py +0 -0
  118. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/middlewares.py +0 -0
  119. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/ocean.py +0 -0
  120. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/py.typed +0 -0
  121. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/run.py +0 -0
  122. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/sonar-project.properties +0 -0
  123. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/utils/__init__.py +0 -0
  124. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/utils/async_http.py +0 -0
  125. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/utils/cache.py +0 -0
  126. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/utils/misc.py +0 -0
  127. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/utils/repeat.py +0 -0
  128. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/utils/signal.py +0 -0
  129. {port_ocean-0.5.10 → port_ocean-0.5.13}/port_ocean/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.5.10
3
+ Version: 0.5.13
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
@@ -21,11 +21,12 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Topic :: Utilities
23
23
  Provides-Extra: cli
24
+ Requires-Dist: aiostream (>=0.5.2,<0.6.0)
24
25
  Requires-Dist: click (>=8.1.3,<9.0.0) ; extra == "cli"
25
26
  Requires-Dist: confluent-kafka (>=2.1.1,<3.0.0)
26
27
  Requires-Dist: cookiecutter (>=2.1.1,<3.0.0) ; extra == "cli"
27
28
  Requires-Dist: fastapi (>=0.100,<0.110)
28
- Requires-Dist: httpx (>=0.24.1,<0.25.0)
29
+ Requires-Dist: httpx (>=0.24.1,<0.28.0)
29
30
  Requires-Dist: jinja2-time (>=0.2.0,<0.3.0) ; extra == "cli"
30
31
  Requires-Dist: loguru (>=0.7.0,<0.8.0)
31
32
  Requires-Dist: pydantic (>=1.10.8,<2.0.0)
@@ -37,7 +38,7 @@ Requires-Dist: rich (>=13.4.1,<14.0.0) ; extra == "cli"
37
38
  Requires-Dist: six (>=1.16.0,<2.0.0)
38
39
  Requires-Dist: tomli (>=2.0.1,<3.0.0)
39
40
  Requires-Dist: urllib3 (>=1.26.16,<2.0.0)
40
- Requires-Dist: uvicorn (>=0.22.0,<0.23.0)
41
+ Requires-Dist: uvicorn (>=0.22,<0.30)
41
42
  Requires-Dist: werkzeug (>=2.3.4,<4.0.0)
42
43
  Project-URL: Repository, https://github.com/port-labs/Port-Ocean
43
44
  Description-Content-Type: text/markdown
@@ -49,7 +49,9 @@ class PortAuthentication:
49
49
 
50
50
  credentials = {"clientId": client_id, "clientSecret": client_secret}
51
51
  response = await self.client.post(
52
- f"{self.api_url}/auth/access_token", json=credentials
52
+ f"{self.api_url}/auth/access_token",
53
+ json=credentials,
54
+ extensions={"retryable": True},
53
55
  )
54
56
  handle_status_code(response)
55
57
  return TokenResponse(**response.json())
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from typing import Any
2
3
  from urllib.parse import quote_plus
3
4
 
4
5
  import httpx
@@ -133,23 +134,10 @@ class EntityClientMixin:
133
134
  return_exceptions=True,
134
135
  )
135
136
 
136
- async def validate_entity_exist(self, identifier: str, blueprint: str) -> None:
137
- logger.info(f"Validating entity {identifier} of blueprint {blueprint} exists")
138
-
139
- response = await self.client.get(
140
- f"{self.auth.api_url}/blueprints/{blueprint}/entities/{identifier}",
141
- headers=await self.auth.headers(),
142
- )
143
- if response.is_error:
144
- logger.error(
145
- f"Error validating "
146
- f"entity: {identifier} of "
147
- f"blueprint: {blueprint}"
148
- )
149
- handle_status_code(response)
150
-
151
- async def search_entities(self, user_agent_type: UserAgentType) -> list[Entity]:
152
- query = {
137
+ async def search_entities(
138
+ self, user_agent_type: UserAgentType, query: dict[Any, Any] | None = None
139
+ ) -> list[Entity]:
140
+ default_query = {
153
141
  "combinator": "and",
154
142
  "rules": [
155
143
  {
@@ -165,6 +153,11 @@ class EntityClientMixin:
165
153
  ],
166
154
  }
167
155
 
156
+ if query is None:
157
+ query = default_query
158
+ elif query.get("rules"):
159
+ query["rules"].append(default_query)
160
+
168
161
  logger.info(f"Searching entities with query {query}")
169
162
  response = await self.client.post(
170
163
  f"{self.auth.api_url}/entities/search",
@@ -174,43 +167,33 @@ class EntityClientMixin:
174
167
  "exclude_calculated_properties": "true",
175
168
  "include": ["blueprint", "identifier"],
176
169
  },
170
+ extensions={"retryable": True},
171
+ timeout=30,
177
172
  )
178
173
  handle_status_code(response)
179
174
  return [Entity.parse_obj(result) for result in response.json()["entities"]]
180
175
 
181
- async def search_dependent_entities(self, entity: Entity) -> list[Entity]:
182
- body = {
183
- "combinator": "and",
184
- "rules": [
185
- {
186
- "operator": "relatedTo",
187
- "blueprint": entity.blueprint,
188
- "value": entity.identifier,
189
- "direction": "downstream",
190
- }
191
- ],
192
- }
193
-
194
- logger.info(f"Searching dependent entity with body {body}")
195
- response = await self.client.post(
196
- f"{self.auth.api_url}/entities/search",
197
- headers=await self.auth.headers(),
198
- json=body,
199
- )
200
- handle_status_code(response)
201
-
202
- return [Entity.parse_obj(result) for result in response.json()["entities"]]
203
-
204
- async def validate_entity_payload(
205
- self, entity: Entity, merge: bool, create_missing_related_entities: bool
206
- ) -> None:
207
- logger.info(f"Validating entity {entity.identifier}")
208
- await self.upsert_entity(
209
- entity,
176
+ async def does_integration_has_ownership_over_entity(
177
+ self, entity: Entity, user_agent_type: UserAgentType
178
+ ) -> bool:
179
+ logger.info(f"Validating ownership on entity {entity.identifier}")
180
+ found_entities: list[Entity] = await self.search_entities(
181
+ user_agent_type,
210
182
  {
211
- "merge": merge,
212
- "create_missing_related_entities": create_missing_related_entities,
213
- "delete_dependent_entities": False,
214
- "validation_only": True,
183
+ "combinator": "and",
184
+ "rules": [
185
+ {
186
+ "property": "$identifier",
187
+ "operator": "contains",
188
+ "value": entity.identifier,
189
+ },
190
+ {
191
+ "property": "$blueprint",
192
+ "operator": "contains",
193
+ "value": entity.blueprint,
194
+ },
195
+ ],
215
196
  },
216
197
  )
198
+
199
+ return len(found_entities) > 0
@@ -15,11 +15,6 @@ class TokenRetryTransport(RetryTransport):
15
15
  super().__init__(**kwargs)
16
16
  self.port_client = port_client
17
17
 
18
- def _is_retryable_method(self, request: httpx.Request) -> bool:
19
- return super()._is_retryable_method(request) or request.url.path.endswith(
20
- "/auth/access_token"
21
- )
22
-
23
18
  async def _handle_unauthorized(self, response: httpx.Response) -> None:
24
19
  token = await self.port_client.auth.token
25
20
  response.headers["Authorization"] = f"Bearer {token}"
@@ -1,6 +1,3 @@
1
- import asyncio
2
- from itertools import chain
3
-
4
1
  from loguru import logger
5
2
 
6
3
  from port_ocean.clients.port.types import UserAgentType
@@ -14,14 +11,9 @@ from port_ocean.core.handlers.entities_state_applier.port.get_related_entities i
14
11
  from port_ocean.core.handlers.entities_state_applier.port.order_by_entities_dependencies import (
15
12
  order_by_entities_dependencies,
16
13
  )
17
- from port_ocean.core.handlers.entities_state_applier.port.validate_entity_relations import (
18
- validate_entity_relations,
19
- )
20
- from port_ocean.core.handlers.entity_processor.base import EntityPortDiff
21
14
  from port_ocean.core.models import Entity
22
15
  from port_ocean.core.ocean_types import EntityDiff
23
- from port_ocean.core.utils import is_same_entity, get_unique, get_port_diff
24
- from port_ocean.exceptions.core import RelationValidationException
16
+ from port_ocean.core.utils import is_same_entity, get_port_diff
25
17
 
26
18
 
27
19
  class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
@@ -32,63 +24,17 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
32
24
  through HTTP requests.
33
25
  """
34
26
 
35
- async def _validate_delete_dependent_entities(self, entities: list[Entity]) -> None:
36
- logger.info("Validated deleted entities")
37
- if not event.port_app_config.delete_dependent_entities:
38
- dependent_entities = await asyncio.gather(
39
- *(
40
- self.context.port_client.search_dependent_entities(entity)
41
- for entity in entities
42
- )
43
- )
44
- new_dependent_entities = get_unique(
45
- [
46
- entity
47
- for entity in chain.from_iterable(dependent_entities)
48
- if not any(is_same_entity(item, entity) for item in entities)
49
- ]
50
- )
51
-
52
- if new_dependent_entities:
53
- raise RelationValidationException(
54
- f"Must enable delete_dependent_entities flag or delete all dependent entities: "
55
- f" {[(dep.blueprint, dep.identifier) for dep in new_dependent_entities]}"
56
- )
57
-
58
- async def _validate_entity_diff(self, diff: EntityPortDiff) -> None:
59
- config = event.port_app_config
60
- await self._validate_delete_dependent_entities(diff.deleted)
61
- modified_or_created_entities = diff.modified + diff.created
62
-
63
- if modified_or_created_entities and not config.create_missing_related_entities:
64
- logger.info("Validating modified or created entities")
65
-
66
- await asyncio.gather(
67
- *(
68
- self.context.port_client.validate_entity_payload(
69
- entity,
70
- config.enable_merge_entity,
71
- create_missing_related_entities=config.create_missing_related_entities,
72
- )
73
- for entity in modified_or_created_entities
74
- )
75
- )
76
-
77
- if not event.port_app_config.delete_dependent_entities:
78
- logger.info("Validating no relation blocks the operation")
79
- await validate_entity_relations(diff, self.context.port_client)
80
-
81
- async def _delete_diff(
27
+ async def _safe_delete(
82
28
  self,
83
29
  entities_to_delete: list[Entity],
84
- created_entities: list[Entity],
30
+ entities_to_protect: list[Entity],
85
31
  user_agent_type: UserAgentType,
86
32
  ) -> None:
87
33
  if not entities_to_delete:
88
34
  return
89
35
 
90
36
  related_entities = await get_related_entities(
91
- created_entities, self.context.port_client
37
+ entities_to_protect, self.context.port_client
92
38
  )
93
39
 
94
40
  allowed_entities_to_delete = []
@@ -98,7 +44,8 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
98
44
  is_same_entity(entity, entity_to_delete) for entity in related_entities
99
45
  )
100
46
  is_part_of_created = any(
101
- is_same_entity(entity, entity_to_delete) for entity in created_entities
47
+ is_same_entity(entity, entity_to_delete)
48
+ for entity in entities_to_protect
102
49
  )
103
50
  if is_part_of_related:
104
51
  if event.port_app_config.create_missing_related_entities:
@@ -119,21 +66,14 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
119
66
  user_agent_type: UserAgentType,
120
67
  ) -> None:
121
68
  diff = get_port_diff(entities["before"], entities["after"])
69
+ kept_entities = diff.created + diff.modified
122
70
 
123
71
  logger.info(
124
72
  f"Updating entity diff (created: {len(diff.created)}, deleted: {len(diff.deleted)}, modified: {len(diff.modified)})"
125
73
  )
126
- await self._validate_entity_diff(diff)
74
+ await self.upsert(kept_entities, user_agent_type)
127
75
 
128
- logger.info("Upserting new entities")
129
- await self.upsert(diff.created, user_agent_type)
130
- logger.info("Upserting modified entities")
131
- await self.upsert(diff.modified, user_agent_type)
132
-
133
- logger.info("Deleting diff entities")
134
- await self._delete_diff(
135
- diff.deleted, diff.created + diff.modified, user_agent_type
136
- )
76
+ await self._safe_delete(diff.deleted, kept_entities, user_agent_type)
137
77
 
138
78
  async def delete_diff(
139
79
  self,
@@ -145,15 +85,13 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
145
85
  if not diff.deleted:
146
86
  return
147
87
 
88
+ kept_entities = diff.created + diff.modified
89
+
148
90
  logger.info(
149
- f"Updating entity diff (created: {len(diff.created)}, deleted: {len(diff.deleted)}, modified: {len(diff.modified)})"
91
+ f"Determining entities to delete ({len(diff.deleted)}/{len(kept_entities)})"
150
92
  )
151
- await self._validate_entity_diff(diff)
152
93
 
153
- logger.info("Deleting diff entities")
154
- await self._delete_diff(
155
- diff.deleted, diff.created + diff.modified, user_agent_type
156
- )
94
+ await self._safe_delete(diff.deleted, kept_entities, user_agent_type)
157
95
 
158
96
  async def upsert(
159
97
  self, entities: list[Entity], user_agent_type: UserAgentType
@@ -6,7 +6,10 @@ from loguru import logger
6
6
  from port_ocean.core.handlers.base import BaseHandler
7
7
  from port_ocean.core.handlers.port_app_config.models import ResourceConfig
8
8
  from port_ocean.core.models import Entity
9
- from port_ocean.core.ocean_types import RawEntityDiff, EntityDiff
9
+ from port_ocean.core.ocean_types import (
10
+ RawEntity,
11
+ EntitySelectorDiff,
12
+ )
10
13
 
11
14
 
12
15
  @dataclass
@@ -33,21 +36,28 @@ class BaseEntityProcessor(BaseHandler):
33
36
 
34
37
  @abstractmethod
35
38
  async def _parse_items(
36
- self, mapping: ResourceConfig, raw_data: RawEntityDiff
37
- ) -> EntityDiff:
39
+ self,
40
+ mapping: ResourceConfig,
41
+ raw_data: list[RawEntity],
42
+ parse_all: bool = False,
43
+ ) -> EntitySelectorDiff:
38
44
  pass
39
45
 
40
46
  async def parse_items(
41
- self, mapping: ResourceConfig, raw_data: RawEntityDiff
42
- ) -> EntityDiff:
47
+ self,
48
+ mapping: ResourceConfig,
49
+ raw_data: list[RawEntity],
50
+ parse_all: bool = False,
51
+ ) -> EntitySelectorDiff:
43
52
  """Public method to parse raw entity data and map it to an EntityDiff.
44
53
 
45
54
  Args:
46
55
  mapping (ResourceConfig): The configuration for entity mapping.
47
- raw_data (RawEntityDiff): The raw data to be parsed.
56
+ raw_data (list[RawEntity]): The raw data to be parsed.
57
+ parse_all (bool): Whether to parse all data or just data that passed the selector.
48
58
 
49
59
  Returns:
50
60
  EntityDiff: The parsed entity differences.
51
61
  """
52
62
  with logger.contextualize(kind=mapping.kind):
53
- return await self._parse_items(mapping, raw_data)
63
+ return await self._parse_items(mapping, raw_data, parse_all)
@@ -9,7 +9,10 @@ import pyjq as jq # type: ignore
9
9
  from port_ocean.core.handlers.entity_processor.base import BaseEntityProcessor
10
10
  from port_ocean.core.handlers.port_app_config.models import ResourceConfig
11
11
  from port_ocean.core.models import Entity
12
- from port_ocean.core.ocean_types import RawEntityDiff, EntityDiff
12
+ from port_ocean.core.ocean_types import (
13
+ RawEntity,
14
+ EntitySelectorDiff,
15
+ )
13
16
  from port_ocean.exceptions.core import EntityProcessorException
14
17
 
15
18
 
@@ -68,16 +71,19 @@ class JQEntityProcessor(BaseEntityProcessor):
68
71
 
69
72
  return result
70
73
 
71
- async def _get_entity_if_passed_selector(
74
+ async def _get_mapped_entity(
72
75
  self,
73
76
  data: dict[str, Any],
74
77
  raw_entity_mappings: dict[str, Any],
75
78
  selector_query: str,
76
- ) -> dict[str, Any]:
79
+ parse_all: bool = False,
80
+ ) -> tuple[dict[str, Any], bool]:
77
81
  should_run = await self._search_as_bool(data, selector_query)
78
- if should_run:
79
- return await self._search_as_object(data, raw_entity_mappings)
80
- return {}
82
+ if parse_all or should_run:
83
+ mapped_entity = await self._search_as_object(data, raw_entity_mappings)
84
+ return mapped_entity, should_run
85
+
86
+ return {}, False
81
87
 
82
88
  async def _calculate_entity(
83
89
  self,
@@ -85,16 +91,18 @@ class JQEntityProcessor(BaseEntityProcessor):
85
91
  raw_entity_mappings: dict[str, Any],
86
92
  items_to_parse: str | None,
87
93
  selector_query: str,
88
- ) -> list[dict[str, Any]]:
94
+ parse_all: bool = False,
95
+ ) -> list[tuple[dict[str, Any], bool]]:
89
96
  if items_to_parse:
90
97
  items = await self._search(data, items_to_parse)
91
98
  if isinstance(items, list):
92
99
  return await asyncio.gather(
93
100
  *[
94
- self._get_entity_if_passed_selector(
101
+ self._get_mapped_entity(
95
102
  {"item": item, **data},
96
103
  raw_entity_mappings,
97
104
  selector_query,
105
+ parse_all,
98
106
  )
99
107
  for item in items
100
108
  ]
@@ -105,51 +113,44 @@ class JQEntityProcessor(BaseEntityProcessor):
105
113
  )
106
114
  else:
107
115
  return [
108
- await self._get_entity_if_passed_selector(
109
- data, raw_entity_mappings, selector_query
116
+ await self._get_mapped_entity(
117
+ data, raw_entity_mappings, selector_query, parse_all
110
118
  )
111
119
  ]
112
- return [{}]
120
+ return [({}, False)]
113
121
 
114
- async def _calculate_entities(
115
- self, mapping: ResourceConfig, raw_data: list[dict[str, Any]]
116
- ) -> list[Entity]:
122
+ async def _parse_items(
123
+ self,
124
+ mapping: ResourceConfig,
125
+ raw_results: list[RawEntity],
126
+ parse_all: bool = False,
127
+ ) -> EntitySelectorDiff:
117
128
  raw_entity_mappings: dict[str, Any] = mapping.port.entity.mappings.dict(
118
129
  exclude_unset=True
119
130
  )
120
- entities_tasks = [
131
+ calculate_entities_tasks = [
121
132
  asyncio.create_task(
122
133
  self._calculate_entity(
123
134
  data,
124
135
  raw_entity_mappings,
125
136
  mapping.port.items_to_parse,
126
137
  mapping.selector.query,
138
+ parse_all,
127
139
  )
128
140
  )
129
- for data in raw_data
130
- ]
131
- entities = await asyncio.gather(*entities_tasks)
132
-
133
- return [
134
- Entity.parse_obj(entity_data)
135
- for flatten in entities
136
- for entity_data in filter(
137
- lambda entity: entity.get("identifier") and entity.get("blueprint"),
138
- flatten,
139
- )
141
+ for data in raw_results
140
142
  ]
141
-
142
- async def _parse_items(
143
- self, mapping: ResourceConfig, raw_results: RawEntityDiff
144
- ) -> EntityDiff:
145
- entities_before: list[Entity] = await self._calculate_entities(
146
- mapping, raw_results["before"]
147
- )
148
- entities_after: list[Entity] = await self._calculate_entities(
149
- mapping, raw_results["after"]
150
- )
151
-
152
- return {
153
- "before": entities_before,
154
- "after": entities_after,
155
- }
143
+ calculate_entities_results = await asyncio.gather(*calculate_entities_tasks)
144
+
145
+ passed_entities = []
146
+ failed_entities = []
147
+ for entities_results in calculate_entities_results:
148
+ for entity, did_entity_pass_selector in entities_results:
149
+ if entity.get("identifier") and entity.get("blueprint"):
150
+ parsed_entity = Entity.parse_obj(entity)
151
+ if did_entity_pass_selector:
152
+ passed_entities.append(parsed_entity)
153
+ else:
154
+ failed_entities.append(parsed_entity)
155
+
156
+ return {"passed": passed_entities, "failed": failed_entities}
@@ -24,6 +24,8 @@ from port_ocean.core.ocean_types import (
24
24
  RawEntityDiff,
25
25
  EntityDiff,
26
26
  ASYNC_GENERATOR_RESYNC_TYPE,
27
+ RawEntity,
28
+ EntitySelectorDiff,
27
29
  )
28
30
  from port_ocean.core.utils import zip_and_sum
29
31
  from port_ocean.exceptions.core import OceanAbortException
@@ -120,12 +122,13 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
120
122
  return results, errors
121
123
 
122
124
  async def _calculate_raw(
123
- self, raw_diff: list[tuple[ResourceConfig, RawEntityDiff]]
124
- ) -> list[EntityDiff]:
125
- logger.info("Calculating diff in entities between states")
125
+ self,
126
+ raw_diff: list[tuple[ResourceConfig, list[RawEntity]]],
127
+ parse_all: bool = False,
128
+ ) -> list[EntitySelectorDiff]:
126
129
  return await asyncio.gather(
127
130
  *(
128
- self.entity_processor.parse_items(mapping, results)
131
+ self.entity_processor.parse_items(mapping, results, parse_all)
129
132
  for mapping, results in raw_diff
130
133
  )
131
134
  )
@@ -135,42 +138,41 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
135
138
  resource: ResourceConfig,
136
139
  results: list[dict[Any, Any]],
137
140
  user_agent_type: UserAgentType,
141
+ parse_all: bool = False,
138
142
  ) -> list[Entity]:
139
- objects_diff = await self._calculate_raw(
140
- [
141
- (
142
- resource,
143
- {
144
- "before": [],
145
- "after": results,
146
- },
147
- )
148
- ]
149
- )
143
+ objects_diff = await self._calculate_raw([(resource, results)], parse_all)
150
144
 
151
- entities_after: list[Entity] = objects_diff[0]["after"]
145
+ entities_after: list[Entity] = objects_diff[0]["passed"]
152
146
  await self.entities_state_applier.upsert(entities_after, user_agent_type)
147
+
148
+ # If an entity didn't pass the JQ selector, we want to delete it if it exists in Port
149
+ for entity_to_delete in objects_diff[0]["failed"]:
150
+ is_owner = (
151
+ await ocean.port_client.does_integration_has_ownership_over_entity(
152
+ entity_to_delete, user_agent_type
153
+ )
154
+ )
155
+ if not is_owner:
156
+ logger.info(
157
+ f"Skipping deletion of entity {entity_to_delete.identifier}, "
158
+ f"Couldn't find an entity that's related to the current integration."
159
+ )
160
+ continue
161
+ await self.entities_state_applier.delete(
162
+ objects_diff[0]["failed"], user_agent_type
163
+ )
164
+
153
165
  return entities_after
154
166
 
155
167
  async def _unregister_resource_raw(
156
168
  self,
157
169
  resource: ResourceConfig,
158
- results: list[dict[Any, Any]],
170
+ results: list[RawEntity],
159
171
  user_agent_type: UserAgentType,
160
172
  ) -> list[Entity]:
161
- objects_diff = await self._calculate_raw(
162
- [
163
- (
164
- resource,
165
- {
166
- "before": results,
167
- "after": [],
168
- },
169
- )
170
- ]
171
- )
173
+ objects_diff = await self._calculate_raw([(resource, results)])
172
174
 
173
- entities_after: list[Entity] = objects_diff[0]["before"]
175
+ entities_after: list[Entity] = objects_diff[0]["passed"]
174
176
  await self.entities_state_applier.delete(entities_after, user_agent_type)
175
177
  logger.info("Finished unregistering change")
176
178
  return entities_after
@@ -233,7 +235,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
233
235
 
234
236
  return await asyncio.gather(
235
237
  *(
236
- self._register_resource_raw(resource, results, user_agent_type)
238
+ self._register_resource_raw(resource, results, user_agent_type, True)
237
239
  for resource in resource_mappings
238
240
  )
239
241
  )
@@ -293,19 +295,31 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
293
295
  with logger.contextualize(kind=kind):
294
296
  logger.info(f"Found {len(resource_mappings)} resources for {kind}")
295
297
 
296
- objects_diff = await self._calculate_raw(
297
- [(mapping, raw_desired_state) for mapping in resource_mappings]
298
+ entities_before = await self._calculate_raw(
299
+ [
300
+ (mapping, raw_desired_state["before"])
301
+ for mapping in resource_mappings
302
+ ]
298
303
  )
299
304
 
300
- entities_before, entities_after = zip_and_sum(
301
- (
302
- (entities_change["before"], entities_change["after"])
303
- for entities_change in objects_diff
304
- )
305
+ entities_after = await self._calculate_raw(
306
+ [(mapping, raw_desired_state["after"]) for mapping in resource_mappings]
305
307
  )
306
308
 
309
+ entities_before_flatten = [
310
+ item
311
+ for sublist in [d["passed"] for d in entities_before]
312
+ for item in sublist
313
+ ]
314
+ entities_after_flatten = [
315
+ item
316
+ for sublist in [d["passed"] for d in entities_after]
317
+ for item in sublist
318
+ ]
319
+
307
320
  await self.entities_state_applier.apply_diff(
308
- {"before": entities_before, "after": entities_after}, user_agent_type
321
+ {"before": entities_before_flatten, "after": entities_after_flatten},
322
+ user_agent_type,
309
323
  )
310
324
 
311
325
  async def sync_raw_all(
@@ -333,8 +347,6 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
333
347
  ):
334
348
  app_config = await self.port_app_config_handler.get_port_app_config()
335
349
 
336
- entities_at_port = await ocean.port_client.search_entities(user_agent_type)
337
-
338
350
  creation_results: list[tuple[list[Entity], list[Exception]]] = []
339
351
 
340
352
  try:
@@ -369,8 +381,11 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
369
381
 
370
382
  logger.error(message, exc_info=error_group)
371
383
  else:
384
+ entities_at_port = await ocean.port_client.search_entities(
385
+ user_agent_type
386
+ )
372
387
  logger.info(
373
- 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)}"
388
+ f"Running resync diff calculation, number of entities found at Port: {len(entities_at_port)}, number of entities found during sync: {len(flat_created_entities)}"
374
389
  )
375
390
  await self.entities_state_applier.delete_diff(
376
391
  {"before": entities_at_port, "after": flat_created_entities},