infrahub-server 1.1.6__py3-none-any.whl → 1.2.0rc0__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.
Files changed (346) hide show
  1. infrahub/api/artifact.py +16 -4
  2. infrahub/api/dependencies.py +8 -0
  3. infrahub/api/oauth2.py +0 -1
  4. infrahub/api/oidc.py +0 -1
  5. infrahub/api/query.py +18 -7
  6. infrahub/api/schema.py +32 -6
  7. infrahub/api/transformation.py +12 -5
  8. infrahub/{message_bus/messages/check_artifact_create.py → artifacts/models.py} +2 -4
  9. infrahub/{message_bus/operations/check/artifact.py → artifacts/tasks.py} +26 -25
  10. infrahub/cli/__init__.py +0 -2
  11. infrahub/cli/db.py +6 -7
  12. infrahub/cli/events.py +8 -3
  13. infrahub/cli/git_agent.py +9 -7
  14. infrahub/cli/tasks.py +4 -6
  15. infrahub/computed_attribute/tasks.py +63 -17
  16. infrahub/computed_attribute/triggers.py +90 -0
  17. infrahub/config.py +1 -1
  18. infrahub/context.py +39 -0
  19. infrahub/core/account.py +5 -8
  20. infrahub/core/attribute.py +53 -21
  21. infrahub/core/branch/models.py +4 -4
  22. infrahub/core/branch/tasks.py +89 -130
  23. infrahub/core/changelog/__init__.py +0 -0
  24. infrahub/core/changelog/diff.py +232 -0
  25. infrahub/core/changelog/models.py +488 -0
  26. infrahub/core/constants/__init__.py +19 -2
  27. infrahub/core/constants/infrahubkind.py +1 -0
  28. infrahub/core/diff/combiner.py +12 -8
  29. infrahub/core/diff/coordinator.py +49 -70
  30. infrahub/core/diff/data_check_synchronizer.py +86 -7
  31. infrahub/core/diff/enricher/aggregated.py +3 -3
  32. infrahub/core/diff/enricher/cardinality_one.py +2 -7
  33. infrahub/core/diff/enricher/hierarchy.py +5 -3
  34. infrahub/core/diff/enricher/labels.py +14 -4
  35. infrahub/core/diff/enricher/path_identifier.py +3 -9
  36. infrahub/core/diff/enricher/summary_counts.py +3 -1
  37. infrahub/core/diff/merger/merger.py +8 -4
  38. infrahub/core/diff/model/path.py +47 -29
  39. infrahub/core/diff/query/all_conflicts.py +6 -3
  40. infrahub/core/diff/query/artifact.py +1 -1
  41. infrahub/core/diff/query/delete_query.py +1 -1
  42. infrahub/core/diff/query/diff_get.py +3 -2
  43. infrahub/core/diff/query/diff_summary.py +1 -1
  44. infrahub/core/diff/query/field_specifiers.py +3 -1
  45. infrahub/core/diff/query/field_summary.py +3 -2
  46. infrahub/core/diff/query/filters.py +12 -1
  47. infrahub/core/diff/query/get_conflict_query.py +1 -1
  48. infrahub/core/diff/query/has_conflicts_query.py +6 -3
  49. infrahub/core/diff/query/merge.py +3 -3
  50. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +4 -4
  51. infrahub/core/diff/query/roots_metadata.py +9 -2
  52. infrahub/core/diff/query/save.py +151 -66
  53. infrahub/core/diff/query/summary_counts_enricher.py +220 -0
  54. infrahub/core/diff/query/time_range_query.py +3 -2
  55. infrahub/core/diff/query/update_conflict_query.py +1 -1
  56. infrahub/core/diff/query_parser.py +49 -24
  57. infrahub/core/diff/repository/deserializer.py +24 -25
  58. infrahub/core/diff/repository/repository.py +76 -20
  59. infrahub/core/diff/tasks.py +9 -8
  60. infrahub/core/enums.py +1 -1
  61. infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
  62. infrahub/core/ipam/reconciler.py +1 -1
  63. infrahub/core/ipam/tasks.py +2 -3
  64. infrahub/core/manager.py +18 -13
  65. infrahub/core/merge.py +5 -2
  66. infrahub/core/migrations/graph/m001_add_version_to_graph.py +1 -1
  67. infrahub/core/migrations/graph/m002_attribute_is_default.py +2 -2
  68. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +2 -2
  69. infrahub/core/migrations/graph/m004_add_attr_documentation.py +1 -1
  70. infrahub/core/migrations/graph/m005_add_rel_read_only.py +1 -1
  71. infrahub/core/migrations/graph/m006_add_rel_on_delete.py +1 -1
  72. infrahub/core/migrations/graph/m007_add_rel_allow_override.py +1 -1
  73. infrahub/core/migrations/graph/m008_add_human_friendly_id.py +1 -1
  74. infrahub/core/migrations/graph/m009_add_generate_profile_attr.py +1 -1
  75. infrahub/core/migrations/graph/m010_add_generate_profile_attr_generic.py +1 -1
  76. infrahub/core/migrations/graph/m011_remove_profile_relationship_schema.py +2 -2
  77. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -23
  78. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -11
  79. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +2 -2
  80. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -1
  81. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -1
  82. infrahub/core/migrations/graph/m017_add_core_profile.py +1 -1
  83. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -2
  84. infrahub/core/migrations/query/attribute_add.py +1 -1
  85. infrahub/core/migrations/query/attribute_rename.py +1 -1
  86. infrahub/core/migrations/query/delete_element_in_schema.py +1 -1
  87. infrahub/core/migrations/query/node_duplicate.py +1 -1
  88. infrahub/core/migrations/query/relationship_duplicate.py +1 -1
  89. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  90. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  91. infrahub/core/migrations/schema/node_remove.py +1 -1
  92. infrahub/core/migrations/schema/tasks.py +5 -5
  93. infrahub/core/migrations/shared.py +4 -4
  94. infrahub/core/models.py +7 -8
  95. infrahub/core/node/__init__.py +161 -40
  96. infrahub/core/node/base.py +1 -1
  97. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  98. infrahub/core/node/delete_validator.py +4 -4
  99. infrahub/core/node/ipam.py +13 -8
  100. infrahub/core/node/permissions.py +4 -0
  101. infrahub/core/node/resource_manager/ip_prefix_pool.py +8 -5
  102. infrahub/core/node/standard.py +3 -5
  103. infrahub/core/property.py +1 -1
  104. infrahub/core/protocols.py +4 -0
  105. infrahub/core/protocols_base.py +4 -2
  106. infrahub/core/query/__init__.py +2 -5
  107. infrahub/core/query/attribute.py +9 -9
  108. infrahub/core/query/branch.py +5 -5
  109. infrahub/core/query/delete.py +1 -1
  110. infrahub/core/query/diff.py +45 -7
  111. infrahub/core/query/ipam.py +4 -4
  112. infrahub/core/query/node.py +19 -14
  113. infrahub/core/query/relationship.py +10 -11
  114. infrahub/core/query/resource_manager.py +13 -11
  115. infrahub/core/query/standard_node.py +6 -6
  116. infrahub/core/query/task.py +3 -3
  117. infrahub/core/query/task_log.py +1 -1
  118. infrahub/core/query/utils.py +5 -5
  119. infrahub/core/registry.py +0 -2
  120. infrahub/core/relationship/constraints/count.py +1 -1
  121. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  122. infrahub/core/relationship/model.py +66 -26
  123. infrahub/core/schema/__init__.py +6 -4
  124. infrahub/core/schema/basenode_schema.py +1 -3
  125. infrahub/core/schema/definitions/core.py +14 -2
  126. infrahub/core/schema/definitions/internal.py +16 -0
  127. infrahub/core/schema/generated/genericnode_schema.py +5 -0
  128. infrahub/core/schema/generated/node_schema.py +5 -0
  129. infrahub/core/schema/generic_schema.py +5 -1
  130. infrahub/core/schema/manager.py +45 -42
  131. infrahub/core/schema/node_schema.py +4 -0
  132. infrahub/core/schema/profile_schema.py +4 -0
  133. infrahub/core/schema/relationship_schema.py +2 -2
  134. infrahub/core/schema/schema_branch.py +248 -14
  135. infrahub/core/schema/template_schema.py +36 -0
  136. infrahub/core/task/user_task.py +7 -5
  137. infrahub/core/timestamp.py +1 -1
  138. infrahub/core/utils.py +3 -2
  139. infrahub/core/validators/attribute/choices.py +1 -1
  140. infrahub/core/validators/attribute/enum.py +1 -1
  141. infrahub/core/validators/attribute/kind.py +1 -1
  142. infrahub/core/validators/attribute/length.py +1 -1
  143. infrahub/core/validators/attribute/optional.py +1 -1
  144. infrahub/core/validators/attribute/regex.py +1 -1
  145. infrahub/core/validators/attribute/unique.py +1 -1
  146. infrahub/core/validators/checks_runner.py +37 -0
  147. infrahub/core/validators/node/generate_profile.py +1 -1
  148. infrahub/core/validators/node/hierarchy.py +1 -1
  149. infrahub/core/validators/query.py +1 -1
  150. infrahub/core/validators/relationship/count.py +1 -1
  151. infrahub/core/validators/relationship/optional.py +1 -1
  152. infrahub/core/validators/relationship/peer.py +1 -1
  153. infrahub/core/validators/tasks.py +8 -6
  154. infrahub/core/validators/uniqueness/query.py +20 -17
  155. infrahub/database/__init__.py +15 -2
  156. infrahub/database/memgraph.py +1 -1
  157. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  158. infrahub/dependencies/builder/diff/combiner.py +1 -1
  159. infrahub/dependencies/builder/diff/conflicts_enricher.py +1 -1
  160. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  161. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  162. infrahub/dependencies/builder/diff/enricher/summary_counts.py +1 -1
  163. infrahub/events/branch_action.py +47 -21
  164. infrahub/events/group_action.py +73 -0
  165. infrahub/events/models.py +159 -51
  166. infrahub/events/node_action.py +74 -8
  167. infrahub/events/repository_action.py +8 -8
  168. infrahub/events/schema_action.py +21 -8
  169. infrahub/generators/tasks.py +12 -13
  170. infrahub/git/base.py +3 -5
  171. infrahub/git/constants.py +0 -1
  172. infrahub/git/integrator.py +36 -35
  173. infrahub/git/repository.py +7 -8
  174. infrahub/git/tasks.py +43 -107
  175. infrahub/git_credential/helper.py +2 -3
  176. infrahub/graphql/analyzer.py +572 -11
  177. infrahub/graphql/app.py +34 -26
  178. infrahub/graphql/auth/query_permission_checker/anonymous_checker.py +5 -5
  179. infrahub/graphql/auth/query_permission_checker/default_branch_checker.py +4 -4
  180. infrahub/graphql/auth/query_permission_checker/merge_operation_checker.py +4 -4
  181. infrahub/graphql/auth/query_permission_checker/object_permission_checker.py +28 -35
  182. infrahub/graphql/auth/query_permission_checker/super_admin_checker.py +5 -5
  183. infrahub/graphql/enums.py +1 -1
  184. infrahub/graphql/initialization.py +5 -1
  185. infrahub/graphql/loaders/node.py +2 -2
  186. infrahub/graphql/manager.py +59 -54
  187. infrahub/graphql/mutations/account.py +20 -13
  188. infrahub/graphql/mutations/artifact_definition.py +16 -12
  189. infrahub/graphql/mutations/branch.py +61 -40
  190. infrahub/graphql/mutations/computed_attribute.py +19 -13
  191. infrahub/graphql/mutations/diff.py +37 -9
  192. infrahub/graphql/mutations/diff_conflict.py +9 -8
  193. infrahub/graphql/mutations/graphql_query.py +19 -11
  194. infrahub/graphql/mutations/ipam.py +21 -19
  195. infrahub/graphql/mutations/main.py +197 -44
  196. infrahub/graphql/mutations/menu.py +8 -8
  197. infrahub/graphql/mutations/proposed_change.py +36 -28
  198. infrahub/graphql/mutations/relationship.py +302 -105
  199. infrahub/graphql/mutations/repository.py +41 -35
  200. infrahub/graphql/mutations/resource_manager.py +26 -26
  201. infrahub/graphql/mutations/schema.py +51 -33
  202. infrahub/graphql/mutations/tasks.py +16 -10
  203. infrahub/graphql/parser.py +1 -1
  204. infrahub/graphql/permissions.py +6 -4
  205. infrahub/graphql/queries/account.py +22 -18
  206. infrahub/graphql/queries/branch.py +6 -4
  207. infrahub/graphql/queries/diff/tree.py +48 -42
  208. infrahub/graphql/queries/event.py +112 -0
  209. infrahub/graphql/queries/internal.py +3 -3
  210. infrahub/graphql/queries/ipam.py +23 -18
  211. infrahub/graphql/queries/relationship.py +11 -10
  212. infrahub/graphql/queries/resource_manager.py +43 -27
  213. infrahub/graphql/queries/search.py +9 -8
  214. infrahub/graphql/queries/status.py +12 -9
  215. infrahub/graphql/queries/task.py +11 -9
  216. infrahub/graphql/resolvers/resolver.py +69 -43
  217. infrahub/graphql/resolvers/single_relationship.py +16 -10
  218. infrahub/graphql/schema.py +2 -0
  219. infrahub/graphql/subscription/__init__.py +1 -1
  220. infrahub/graphql/subscription/events.py +1 -1
  221. infrahub/graphql/subscription/graphql_query.py +8 -8
  222. infrahub/graphql/types/branch.py +2 -2
  223. infrahub/graphql/types/common.py +6 -1
  224. infrahub/graphql/types/enums.py +2 -0
  225. infrahub/graphql/types/event.py +100 -0
  226. infrahub/graphql/types/interface.py +2 -2
  227. infrahub/graphql/types/node.py +3 -3
  228. infrahub/graphql/types/permission.py +2 -2
  229. infrahub/graphql/types/relationship.py +3 -3
  230. infrahub/graphql/types/standard_node.py +9 -11
  231. infrahub/graphql/utils.py +28 -182
  232. infrahub/groups/tasks.py +2 -3
  233. infrahub/lock.py +1 -1
  234. infrahub/menu/constants.py +1 -0
  235. infrahub/menu/generator.py +14 -3
  236. infrahub/menu/menu.py +116 -127
  237. infrahub/menu/models.py +4 -4
  238. infrahub/message_bus/messages/__init__.py +0 -4
  239. infrahub/message_bus/messages/event_branch_merge.py +3 -0
  240. infrahub/message_bus/messages/request_proposedchange_pipeline.py +2 -0
  241. infrahub/message_bus/operations/__init__.py +3 -5
  242. infrahub/message_bus/operations/check/__init__.py +2 -2
  243. infrahub/message_bus/operations/check/generator.py +1 -3
  244. infrahub/message_bus/operations/check/repository.py +1 -1
  245. infrahub/message_bus/operations/event/branch.py +7 -3
  246. infrahub/message_bus/operations/event/schema.py +1 -1
  247. infrahub/message_bus/operations/finalize/validator.py +1 -1
  248. infrahub/message_bus/operations/git/file.py +2 -2
  249. infrahub/message_bus/operations/git/repository.py +1 -1
  250. infrahub/message_bus/operations/requests/__init__.py +0 -2
  251. infrahub/message_bus/operations/requests/generator_definition.py +1 -1
  252. infrahub/message_bus/operations/requests/proposed_change.py +26 -11
  253. infrahub/message_bus/operations/requests/repository.py +2 -2
  254. infrahub/message_bus/operations/send/echo.py +1 -1
  255. infrahub/message_bus/types.py +1 -1
  256. infrahub/permissions/__init__.py +2 -1
  257. infrahub/permissions/types.py +26 -0
  258. infrahub/pools/prefix.py +29 -165
  259. infrahub/prefect_server/__init__.py +0 -0
  260. infrahub/prefect_server/app.py +18 -0
  261. infrahub/prefect_server/database.py +20 -0
  262. infrahub/prefect_server/events.py +28 -0
  263. infrahub/prefect_server/models.py +46 -0
  264. infrahub/proposed_change/models.py +15 -1
  265. infrahub/proposed_change/tasks.py +173 -35
  266. infrahub/pytest_plugin.py +4 -4
  267. infrahub/server.py +12 -11
  268. infrahub/services/__init__.py +147 -62
  269. infrahub/services/adapters/cache/__init__.py +7 -5
  270. infrahub/services/adapters/cache/nats.py +40 -22
  271. infrahub/services/adapters/cache/redis.py +0 -4
  272. infrahub/services/adapters/event/__init__.py +10 -18
  273. infrahub/services/adapters/http/__init__.py +0 -5
  274. infrahub/services/adapters/http/httpx.py +22 -15
  275. infrahub/services/adapters/message_bus/__init__.py +23 -6
  276. infrahub/services/adapters/message_bus/local.py +8 -6
  277. infrahub/services/adapters/message_bus/nats.py +12 -6
  278. infrahub/services/adapters/message_bus/rabbitmq.py +22 -9
  279. infrahub/services/adapters/workflow/__init__.py +11 -8
  280. infrahub/services/adapters/workflow/local.py +28 -7
  281. infrahub/services/adapters/workflow/worker.py +23 -7
  282. infrahub/services/component.py +38 -35
  283. infrahub/services/scheduler.py +32 -29
  284. infrahub/storage.py +2 -4
  285. infrahub/task_manager/constants.py +1 -1
  286. infrahub/task_manager/event.py +182 -0
  287. infrahub/task_manager/models.py +125 -1
  288. infrahub/task_manager/task.py +1 -1
  289. infrahub/tasks/artifact.py +14 -16
  290. infrahub/tasks/registry.py +1 -1
  291. infrahub/tasks/telemetry.py +13 -14
  292. infrahub/transformations/tasks.py +3 -5
  293. infrahub/trigger/__init__.py +0 -0
  294. infrahub/trigger/catalogue.py +15 -0
  295. infrahub/trigger/constants.py +9 -0
  296. infrahub/trigger/models.py +69 -0
  297. infrahub/trigger/tasks.py +85 -0
  298. infrahub/types.py +1 -1
  299. infrahub/utils.py +1 -1
  300. infrahub/webhook/constants.py +0 -2
  301. infrahub/webhook/models.py +8 -2
  302. infrahub/webhook/tasks.py +20 -73
  303. infrahub/webhook/triggers.py +20 -0
  304. infrahub/workers/infrahub_async.py +36 -25
  305. infrahub/workers/utils.py +63 -0
  306. infrahub/workflows/catalogue.py +13 -37
  307. infrahub/workflows/initialization.py +6 -8
  308. infrahub/workflows/models.py +3 -5
  309. infrahub/workflows/utils.py +1 -1
  310. infrahub_sdk/ctl/check.py +3 -3
  311. infrahub_sdk/ctl/cli_commands.py +11 -10
  312. infrahub_sdk/ctl/exceptions.py +0 -6
  313. infrahub_sdk/ctl/exporter.py +1 -1
  314. infrahub_sdk/ctl/generator.py +5 -5
  315. infrahub_sdk/ctl/importer.py +3 -2
  316. infrahub_sdk/ctl/menu.py +1 -1
  317. infrahub_sdk/ctl/object.py +1 -1
  318. infrahub_sdk/ctl/repository.py +23 -15
  319. infrahub_sdk/ctl/schema.py +2 -2
  320. infrahub_sdk/ctl/utils.py +4 -3
  321. infrahub_sdk/ctl/validate.py +2 -1
  322. infrahub_sdk/exceptions.py +6 -0
  323. infrahub_sdk/generator.py +3 -0
  324. infrahub_sdk/node.py +2 -2
  325. infrahub_sdk/schema/__init__.py +14 -2
  326. infrahub_sdk/schema/main.py +7 -0
  327. infrahub_sdk/utils.py +11 -1
  328. infrahub_sdk/yaml.py +2 -3
  329. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/METADATA +46 -12
  330. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/RECORD +338 -321
  331. infrahub_testcontainers/container.py +14 -6
  332. infrahub_testcontainers/docker-compose.test.yml +24 -5
  333. infrahub_testcontainers/haproxy.cfg +43 -0
  334. infrahub_testcontainers/helpers.py +85 -1
  335. infrahub/core/branch/constants.py +0 -2
  336. infrahub/graphql/query.py +0 -52
  337. infrahub/message_bus/messages/request_artifactdefinition_check.py +0 -17
  338. infrahub/message_bus/operations/requests/artifact_definition.py +0 -148
  339. infrahub/schema/constants.py +0 -1
  340. infrahub/schema/tasks.py +0 -76
  341. infrahub/services/adapters/database/__init__.py +0 -9
  342. infrahub_sdk/ctl/_file.py +0 -13
  343. /infrahub/{schema → artifacts}/__init__.py +0 -0
  344. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/LICENSE.txt +0 -0
  345. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/WHEEL +0 -0
  346. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,488 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Self, cast
4
+ from uuid import UUID
5
+
6
+ from pydantic import BaseModel, Field, PrivateAttr, computed_field, field_validator, model_validator
7
+
8
+ from infrahub.core.constants import NULL_VALUE, DiffAction, RelationshipCardinality, RelationshipKind
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.core.attribute import BaseAttribute
12
+ from infrahub.core.branch import Branch
13
+ from infrahub.core.manager import RelationshipSchema
14
+ from infrahub.core.query.relationship import RelationshipPeerData
15
+ from infrahub.core.relationship.model import Relationship
16
+ from infrahub.database import InfrahubDatabase
17
+
18
+
19
+ class PropertyChangelog(BaseModel):
20
+ name: str = Field(..., description="The name of the property")
21
+ value: str | bool | None = Field(..., description="The updated or current value of the property")
22
+ value_previous: str | bool | None = Field(
23
+ ...,
24
+ description="The previous value of the property, a `null` value indicates that the property didn't previously have a value",
25
+ )
26
+
27
+ @computed_field
28
+ def value_type(self) -> str:
29
+ """The value_type of the property, used to help external systems"""
30
+ if isinstance(self.value, str):
31
+ return "Text"
32
+
33
+ return "Boolean"
34
+
35
+ @computed_field
36
+ def value_update_status(self) -> DiffAction:
37
+ """Indicate how the value was changed during this update"""
38
+ if self.value == self.value_previous:
39
+ return DiffAction.UNCHANGED
40
+ if self.value_previous is not None and self.value is None:
41
+ return DiffAction.REMOVED
42
+ if self.value_previous is None and self.value is not None:
43
+ return DiffAction.ADDED
44
+
45
+ return DiffAction.UPDATED
46
+
47
+
48
+ class AttributeChangelog(BaseModel):
49
+ name: str = Field(..., description="The name of the attribute")
50
+ value: Any = Field(default=None, description="The current value of the attribute")
51
+ value_previous: Any = Field(default=None, description="The previous value of the attribute")
52
+ properties: dict[str, PropertyChangelog] = Field(
53
+ default_factory=dict, description="The properties that were updated during this update"
54
+ )
55
+ kind: str = Field(..., description="The attribute kind")
56
+
57
+ @computed_field
58
+ def value_update_status(self) -> DiffAction:
59
+ """Indicate how the peer was changed during this update"""
60
+ if self.value == self.value_previous:
61
+ return DiffAction.UNCHANGED
62
+ if self.value_previous is not None and self.value is None:
63
+ return DiffAction.REMOVED
64
+ if self.value_previous is None and self.value is not None:
65
+ return DiffAction.ADDED
66
+
67
+ return DiffAction.UPDATED
68
+
69
+ def add_property(self, name: str, value_current: bool | str | None, value_previous: bool | str | None) -> None:
70
+ self.properties[name] = PropertyChangelog(name=name, value=value_current, value_previous=value_previous)
71
+
72
+ @property
73
+ def has_updates(self) -> bool:
74
+ if self.value_update_status != DiffAction.UNCHANGED or self.properties:
75
+ return True
76
+ return False
77
+
78
+ @field_validator("value", "value_previous")
79
+ @classmethod
80
+ def convert_null_values(cls, value: Any) -> Any:
81
+ if isinstance(value, str) and value == NULL_VALUE:
82
+ return None
83
+ return value
84
+
85
+ @model_validator(mode="after")
86
+ def filter_sensitive(self) -> Self:
87
+ if self.kind in ["HashedPassword", "Password"]:
88
+ if self.value is not None:
89
+ self.value = "***"
90
+ if self.value_previous is not None:
91
+ self.value_previous = "***"
92
+
93
+ return self
94
+
95
+
96
+ class RelationshipCardinalityOneChangelog(BaseModel):
97
+ name: str = Field(..., description="The name of the relationship")
98
+ peer_id_previous: str | None = Field(default=None, description="The previous peer of this relationship")
99
+ peer_kind_previous: str | None = Field(default=None, description="The node kind of the previous peer")
100
+ peer_id: str | None = Field(default=None, description="The current peer of this relationship")
101
+ peer_kind: str | None = Field(default=None, description="The node kind of the current peer")
102
+ properties: dict[str, PropertyChangelog] = Field(
103
+ default_factory=dict, description="Changes to properties of this relationship if any were made"
104
+ )
105
+ _parent: ChangelogRelatedNode | None = PrivateAttr(default=None)
106
+
107
+ @property
108
+ def parent(self) -> ChangelogRelatedNode | None:
109
+ return self._parent
110
+
111
+ @computed_field
112
+ def cardinality(self) -> str:
113
+ return "one"
114
+
115
+ @computed_field
116
+ def peer_status(self) -> DiffAction:
117
+ """Indicate how the peer was changed during this update"""
118
+ if self.peer_id_previous == self.peer_id:
119
+ return DiffAction.UNCHANGED
120
+ if self.peer_id_previous and not self.peer_id:
121
+ return DiffAction.REMOVED
122
+ if self.peer_id and not self.peer_id_previous:
123
+ return DiffAction.ADDED
124
+ return DiffAction.UPDATED
125
+
126
+ def add_property(self, name: str, value_current: bool | str | None, value_previous: bool | str | None) -> None:
127
+ self.properties[name] = PropertyChangelog(name=name, value=value_current, value_previous=value_previous)
128
+
129
+ def set_parent(self, parent_id: str, parent_kind: str) -> None:
130
+ self._parent = ChangelogRelatedNode(node_id=parent_id, node_kind=parent_kind)
131
+
132
+ def set_parent_from_relationship(self, relationship: Relationship) -> None:
133
+ if relationship.schema.kind == RelationshipKind.PARENT:
134
+ if (
135
+ self.peer_status in [DiffAction.ADDED, DiffAction.UNCHANGED, DiffAction.UPDATED]
136
+ and self.peer_id
137
+ and self.peer_kind
138
+ ):
139
+ self._parent = ChangelogRelatedNode(node_id=self.peer_id, node_kind=self.peer_kind)
140
+ elif self.peer_id_previous and self.peer_kind_previous:
141
+ self._parent = ChangelogRelatedNode(node_id=self.peer_id_previous, node_kind=self.peer_kind_previous)
142
+
143
+ @property
144
+ def is_empty(self) -> bool:
145
+ return self.peer_status == DiffAction.UNCHANGED and not self.properties
146
+
147
+
148
+ class RelationshipPeerChangelog(BaseModel):
149
+ peer_id: str = Field(..., description="The ID of the peer")
150
+ peer_kind: str = Field(..., description="The node kind of the peer")
151
+ peer_status: DiffAction = Field(
152
+ ..., description="Indicate how the relationship to this peer was changed in this update"
153
+ )
154
+ properties: dict[str, PropertyChangelog] = Field(
155
+ default_factory=dict, description="Changes to properties of this relationship if any were made"
156
+ )
157
+
158
+ def add_property(self, name: str, value_current: bool | str | None, value_previous: bool | str | None) -> None:
159
+ self.properties[name] = PropertyChangelog(name=name, value=value_current, value_previous=value_previous)
160
+
161
+
162
+ class RelationshipCardinalityManyChangelog(BaseModel):
163
+ name: str
164
+ peers: list[RelationshipPeerChangelog] = Field(default_factory=list)
165
+
166
+ @computed_field
167
+ def cardinality(self) -> str:
168
+ return "many"
169
+
170
+ def add_new_peer(self, relationship: Relationship) -> None:
171
+ properties: dict[str, PropertyChangelog] = {}
172
+ properties["is_protected"] = PropertyChangelog(
173
+ name="is_protected", value=relationship.is_protected, value_previous=None
174
+ )
175
+ properties["is_visible"] = PropertyChangelog(
176
+ name="is_visible", value=relationship.is_protected, value_previous=None
177
+ )
178
+ if owner := getattr(relationship, "owner_id", None):
179
+ properties["owner"] = PropertyChangelog(name="owner", value=owner, value_previous=None)
180
+ if source := getattr(relationship, "source_id", None):
181
+ properties["source"] = PropertyChangelog(name="source_id", value=source, value_previous=None)
182
+
183
+ self.peers.append(
184
+ RelationshipPeerChangelog(
185
+ peer_id=relationship.get_peer_id(),
186
+ peer_kind=relationship.get_peer_kind(),
187
+ peer_status=DiffAction.ADDED,
188
+ properties=properties,
189
+ )
190
+ )
191
+
192
+ def remove_peer(self, peer_id: str, peer_kind: str) -> None:
193
+ self.peers.append(
194
+ RelationshipPeerChangelog(
195
+ peer_id=peer_id,
196
+ peer_kind=peer_kind,
197
+ peer_status=DiffAction.REMOVED,
198
+ )
199
+ )
200
+
201
+ @property
202
+ def is_empty(self) -> bool:
203
+ return not self.peers
204
+
205
+
206
+ class ChangelogRelatedNode(BaseModel):
207
+ node_id: str
208
+ node_kind: str
209
+
210
+
211
+ class NodeChangelog(BaseModel):
212
+ """Emitted when a node is updated"""
213
+
214
+ node_id: str
215
+ node_kind: str
216
+ display_label: str
217
+
218
+ attributes: dict[str, AttributeChangelog] = Field(default_factory=dict)
219
+ relationships: dict[str, RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog] = Field(
220
+ default_factory=dict
221
+ )
222
+
223
+ _parent: ChangelogRelatedNode | None = PrivateAttr(default=None)
224
+
225
+ @property
226
+ def parent(self) -> ChangelogRelatedNode | None:
227
+ return self._parent
228
+
229
+ @property
230
+ def updated_fields(self) -> list[str]:
231
+ """Return a list of update fields i.e. attributes and relationships"""
232
+ return list(self.relationships.keys()) + list(self.attributes.keys())
233
+
234
+ @property
235
+ def has_changes(self) -> bool:
236
+ return len(self.updated_fields) > 0
237
+
238
+ @property
239
+ def root_node_id(self) -> str:
240
+ """Return the top level node_id"""
241
+ if self.parent:
242
+ return self.parent.node_id
243
+ return self.node_id
244
+
245
+ def add_parent(self, parent: ChangelogRelatedNode) -> None:
246
+ self._parent = parent
247
+
248
+ def add_parent_from_relationship(self, parent: Relationship) -> None:
249
+ self._parent = ChangelogRelatedNode(node_id=parent.get_peer_id(), node_kind=parent.get_peer_kind())
250
+
251
+ def create_relationship(self, relationship: Relationship) -> None:
252
+ if relationship.schema.cardinality == RelationshipCardinality.ONE:
253
+ peer_id = relationship.get_peer_id()
254
+ peer_kind = relationship.get_peer_kind()
255
+ if relationship.schema.kind == RelationshipKind.PARENT:
256
+ self._parent = ChangelogRelatedNode(node_id=peer_id, node_kind=peer_kind)
257
+ changelog_relationship = RelationshipCardinalityOneChangelog(
258
+ name=relationship.schema.name,
259
+ peer_id=peer_id,
260
+ peer_kind=peer_kind,
261
+ )
262
+ if source_id := getattr(relationship, "source_id", None):
263
+ changelog_relationship.add_property(name="source", value_current=source_id, value_previous=None)
264
+ if owner_id := getattr(relationship, "owner_id", None):
265
+ changelog_relationship.add_property(name="owner", value_current=owner_id, value_previous=None)
266
+ changelog_relationship.add_property(
267
+ name="is_protected", value_current=relationship.is_protected, value_previous=None
268
+ )
269
+ changelog_relationship.add_property(
270
+ name="is_visible", value_current=relationship.is_visible, value_previous=None
271
+ )
272
+ self.relationships[changelog_relationship.name] = changelog_relationship
273
+ elif relationship.schema.cardinality == RelationshipCardinality.MANY:
274
+ if relationship.schema.name not in self.relationships:
275
+ self.relationships[relationship.schema.name] = RelationshipCardinalityManyChangelog(
276
+ name=relationship.schema.name
277
+ )
278
+ relationship_container = cast(
279
+ RelationshipCardinalityManyChangelog, self.relationships[relationship.schema.name]
280
+ )
281
+
282
+ relationship_container.add_new_peer(relationship=relationship)
283
+
284
+ def delete_relationship(self, relationship: Relationship) -> None:
285
+ if relationship.schema.cardinality == RelationshipCardinality.ONE:
286
+ peer_id = relationship.get_peer_id()
287
+ peer_kind = relationship.get_peer_kind()
288
+ changelog_relationship = RelationshipCardinalityOneChangelog(
289
+ name=relationship.schema.name,
290
+ peer_id_previous=peer_id,
291
+ peer_kind_previous=peer_kind,
292
+ )
293
+ self.relationships[changelog_relationship.name] = changelog_relationship
294
+ elif relationship.schema.cardinality == RelationshipCardinality.MANY:
295
+ if relationship.schema.name not in self.relationships:
296
+ self.relationships[relationship.schema.name] = RelationshipCardinalityManyChangelog(
297
+ name=relationship.schema.name
298
+ )
299
+ relationship_container = cast(
300
+ RelationshipCardinalityManyChangelog, self.relationships[relationship.schema.name]
301
+ )
302
+ relationship_container.remove_peer(
303
+ peer_id=relationship.get_peer_id(), peer_kind=relationship.get_peer_kind()
304
+ )
305
+
306
+ def add_attribute(self, attribute: AttributeChangelog) -> None:
307
+ self.attributes[attribute.name] = attribute
308
+
309
+ def add_relationship(
310
+ self, relationship: RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog
311
+ ) -> None:
312
+ if isinstance(relationship, RelationshipCardinalityOneChangelog) and relationship.parent:
313
+ self.add_parent(parent=relationship.parent)
314
+ if relationship.is_empty:
315
+ return
316
+
317
+ self.relationships[relationship.name] = relationship
318
+
319
+ def create_attribute(self, attribute: BaseAttribute) -> None:
320
+ changelog_attribute = AttributeChangelog(
321
+ name=attribute.name, value=attribute.value, value_previous=None, kind=attribute.schema.kind
322
+ )
323
+ if source_id := getattr(attribute, "source_id", None):
324
+ changelog_attribute.add_property(name="source", value_current=source_id, value_previous=None)
325
+ if owner_id := getattr(attribute, "owner_id", None):
326
+ changelog_attribute.add_property(name="owner", value_current=owner_id, value_previous=None)
327
+ changelog_attribute.add_property(name="is_protected", value_current=attribute.is_protected, value_previous=None)
328
+ changelog_attribute.add_property(name="is_visible", value_current=attribute.is_visible, value_previous=None)
329
+ self.attributes[changelog_attribute.name] = changelog_attribute
330
+
331
+ def get_related_nodes(self) -> list[ChangelogRelatedNode]:
332
+ related_nodes: dict[str, ChangelogRelatedNode] = {}
333
+ for relationship in self.relationships.values():
334
+ if isinstance(relationship, RelationshipCardinalityOneChangelog):
335
+ if relationship.peer_id and relationship.peer_kind:
336
+ related_nodes[relationship.peer_id] = ChangelogRelatedNode(
337
+ node_id=relationship.peer_id, node_kind=relationship.peer_kind
338
+ )
339
+ if relationship.peer_id_previous and relationship.peer_kind_previous:
340
+ related_nodes[relationship.peer_id_previous] = ChangelogRelatedNode(
341
+ node_id=relationship.peer_id_previous, node_kind=relationship.peer_kind_previous
342
+ )
343
+ elif isinstance(relationship, RelationshipCardinalityManyChangelog):
344
+ for peer in relationship.peers:
345
+ related_nodes[peer.peer_id] = ChangelogRelatedNode(node_id=peer.peer_id, node_kind=peer.peer_kind)
346
+
347
+ if self.parent:
348
+ related_nodes[self.parent.node_id] = self.parent
349
+
350
+ return list(related_nodes.values())
351
+
352
+
353
+ class ChangelogRelationshipMapper:
354
+ def __init__(self, schema: RelationshipSchema) -> None:
355
+ self.schema = schema
356
+ self._cardinality_one_relationship: RelationshipCardinalityOneChangelog | None = None
357
+ self._cardinality_many_relationship: RelationshipCardinalityManyChangelog | None = None
358
+
359
+ @property
360
+ def cardinality_one_relationship(self) -> RelationshipCardinalityOneChangelog:
361
+ if not self._cardinality_one_relationship:
362
+ self._cardinality_one_relationship = RelationshipCardinalityOneChangelog(name=self.schema.name)
363
+
364
+ return self._cardinality_one_relationship
365
+
366
+ @property
367
+ def cardinality_many_relationship(self) -> RelationshipCardinalityManyChangelog:
368
+ if not self._cardinality_many_relationship:
369
+ self._cardinality_many_relationship = RelationshipCardinalityManyChangelog(name=self.schema.name)
370
+
371
+ return self._cardinality_many_relationship
372
+
373
+ def remove_peer(self, peer_data: RelationshipPeerData) -> None:
374
+ if self.schema.cardinality == RelationshipCardinality.ONE:
375
+ self.cardinality_one_relationship.peer_id_previous = str(peer_data.peer_id)
376
+ self.cardinality_one_relationship.peer_kind_previous = peer_data.peer_kind
377
+ elif self.schema.cardinality == RelationshipCardinality.MANY:
378
+ self.cardinality_many_relationship.remove_peer(
379
+ peer_id=str(peer_data.peer_id), peer_kind=peer_data.peer_kind
380
+ )
381
+
382
+ def _set_cardinality_one_peer(self, relationship: Relationship) -> None:
383
+ self.cardinality_one_relationship.peer_id = relationship.peer_id
384
+ self.cardinality_one_relationship.peer_kind = relationship.get_peer_kind()
385
+ self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)
386
+
387
+ def add_parent_from_relationship(self, relationship: Relationship) -> None:
388
+ if self.schema.cardinality == RelationshipCardinality.ONE:
389
+ self.cardinality_one_relationship.set_parent(
390
+ parent_id=relationship.get_peer_id(), parent_kind=relationship.get_peer_kind()
391
+ )
392
+
393
+ def add_peer_from_relationship(self, relationship: Relationship) -> None:
394
+ if self.schema.cardinality == RelationshipCardinality.ONE:
395
+ self._set_cardinality_one_peer(relationship=relationship)
396
+ elif self.schema.cardinality == RelationshipCardinality.MANY:
397
+ self.cardinality_many_relationship.add_new_peer(relationship=relationship)
398
+
399
+ def add_updated_relationship(
400
+ self, relationship: Relationship, old_data: RelationshipPeerData, properties_to_update: list[str]
401
+ ) -> None:
402
+ if self.schema.cardinality == RelationshipCardinality.ONE:
403
+ self._set_cardinality_one_peer(relationship=relationship)
404
+ self.cardinality_one_relationship.peer_id_previous = self.cardinality_one_relationship.peer_id
405
+ self.cardinality_one_relationship.peer_kind_previous = self.cardinality_one_relationship.peer_kind
406
+ for property_to_update in properties_to_update:
407
+ previous_property = old_data.properties.get(property_to_update)
408
+ previous_value: str | bool | None = None
409
+ if previous_property:
410
+ if isinstance(previous_property.value, UUID):
411
+ previous_value = str(previous_property.value)
412
+ else:
413
+ previous_value = previous_property.value
414
+ property_name = (
415
+ property_to_update if property_to_update not in ["source", "owner"] else f"{property_to_update}_id"
416
+ )
417
+ self.cardinality_one_relationship.add_property(
418
+ name=property_to_update,
419
+ value_current=getattr(relationship, property_name),
420
+ value_previous=previous_value,
421
+ )
422
+ self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)
423
+
424
+ def delete_relationship(self, relationship: Relationship) -> None:
425
+ if self.schema.cardinality == RelationshipCardinality.ONE:
426
+ self.cardinality_one_relationship.peer_id_previous = relationship.get_peer_id()
427
+ self.cardinality_one_relationship.peer_kind_previous = relationship.get_peer_kind()
428
+ self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)
429
+
430
+ elif self.schema.cardinality == RelationshipCardinality.MANY:
431
+ self.cardinality_many_relationship.remove_peer(
432
+ peer_id=relationship.get_peer_id(), peer_kind=relationship.get_peer_kind()
433
+ )
434
+
435
+ @property
436
+ def changelog(self) -> RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog:
437
+ match self.schema.cardinality:
438
+ case RelationshipCardinality.ONE:
439
+ return self.cardinality_one_relationship
440
+ case RelationshipCardinality.MANY:
441
+ return self.cardinality_many_relationship
442
+
443
+
444
+ class RelationshipChangelogGetter:
445
+ def __init__(self, db: InfrahubDatabase, branch: Branch) -> None:
446
+ self._db = db
447
+ self._branch = branch
448
+
449
+ async def get_changelogs(self, primary_changelog: NodeChangelog) -> list[NodeChangelog]:
450
+ """Return secondary changelogs based on this update
451
+
452
+ These will typically include updates to relationships on other nodes.
453
+ """
454
+ schema_branch = self._db.schema.get_schema_branch(name=self._branch.name)
455
+ node_schema = schema_branch.get(name=primary_changelog.node_kind)
456
+ secondaries: list[NodeChangelog] = []
457
+
458
+ for relationship in primary_changelog.relationships.values():
459
+ rel_schema = node_schema.get_relationship(name=relationship.name)
460
+ if isinstance(relationship, RelationshipCardinalityOneChangelog):
461
+ # For now this code only looks at the scenario when a cardinality=one relationship
462
+ # is added to a node and it has a cardinality=many relationship coming back from
463
+ # another node, it will be expanded to include all variations.
464
+ if relationship.peer_status == DiffAction.ADDED:
465
+ peer_schema = schema_branch.get(name=str(relationship.peer_kind))
466
+ peer_relation = peer_schema.get_relationship_by_identifier(
467
+ id=str(rel_schema.identifier), raise_on_error=False
468
+ )
469
+ if peer_relation:
470
+ node_changelog = NodeChangelog(
471
+ node_id=str(relationship.peer_id),
472
+ node_kind=str(relationship.peer_kind),
473
+ display_label="n/a",
474
+ )
475
+ if peer_relation.cardinality == RelationshipCardinality.MANY:
476
+ node_changelog.relationships[peer_relation.name] = RelationshipCardinalityManyChangelog(
477
+ name=peer_relation.name,
478
+ peers=[
479
+ RelationshipPeerChangelog(
480
+ peer_id=primary_changelog.node_id,
481
+ peer_kind=primary_changelog.node_kind,
482
+ peer_status=DiffAction.ADDED,
483
+ )
484
+ ],
485
+ )
486
+ secondaries.append(node_changelog)
487
+
488
+ return secondaries
@@ -175,11 +175,23 @@ class GeneratorInstanceStatus(InfrahubStringEnum):
175
175
 
176
176
 
177
177
  class MutationAction(InfrahubStringEnum):
178
- ADDED = "added"
179
- REMOVED = "removed"
178
+ CREATED = "created"
179
+ DELETED = "deleted"
180
180
  UPDATED = "updated"
181
181
  UNDEFINED = "undefined"
182
182
 
183
+ @classmethod
184
+ def from_diff_action(cls, diff_action: DiffAction) -> MutationAction:
185
+ match diff_action:
186
+ case DiffAction.ADDED:
187
+ return MutationAction.CREATED
188
+ case DiffAction.REMOVED:
189
+ return MutationAction.DELETED
190
+ case DiffAction.UPDATED:
191
+ return MutationAction.UPDATED
192
+ case DiffAction.UNCHANGED:
193
+ return MutationAction.UNDEFINED
194
+
183
195
 
184
196
  class PathResourceType(InfrahubStringEnum):
185
197
  SCHEMA = "schema"
@@ -225,6 +237,7 @@ class RelationshipKind(InfrahubStringEnum):
225
237
  GROUP = "Group"
226
238
  HIERARCHY = "Hierarchy"
227
239
  PROFILE = "Profile"
240
+ TEMPLATE = "Template"
228
241
 
229
242
 
230
243
  class RelationshipStatus(InfrahubStringEnum):
@@ -301,6 +314,7 @@ RESTRICTED_NAMESPACES: list[str] = [
301
314
  "Lineage",
302
315
  "Schema",
303
316
  "Profile",
317
+ "Template",
304
318
  ]
305
319
 
306
320
  NODE_NAME_REGEX = r"^[A-Z][a-zA-Z0-9]+$"
@@ -315,3 +329,6 @@ DEFAULT_KIND_MAX_LENGTH = 32
315
329
  NAMESPACE_REGEX = r"^[A-Z][a-z0-9]+$"
316
330
  NODE_KIND_REGEX = r"^[A-Z][a-zA-Z0-9]+$"
317
331
  DEFAULT_REL_IDENTIFIER_LENGTH = 128
332
+
333
+ OBJECT_TEMPLATE_RELATIONSHIP_NAME = "object_template"
334
+ OBJECT_TEMPLATE_NAME_ATTR = "template_name"
@@ -42,6 +42,7 @@ NUMBERPOOL = "CoreNumberPool"
42
42
  LINEAGEOWNER = "LineageOwner"
43
43
  LINEAGESOURCE = "LineageSource"
44
44
  OBJECTPERMISSION = "CoreObjectPermission"
45
+ OBJECTTEMPLATE = "CoreObjectTemplate"
45
46
  OBJECTTHREAD = "CoreObjectThread"
46
47
  PASSWORDCREDENTIAL = "CorePasswordCredential"
47
48
  PROFILE = "CoreProfile"
@@ -1,7 +1,6 @@
1
1
  from copy import deepcopy
2
2
  from dataclasses import dataclass, field, replace
3
3
  from typing import Iterable
4
- from uuid import uuid4
5
4
 
6
5
  from infrahub.core.constants import NULL_VALUE, DiffAction, RelationshipCardinality
7
6
  from infrahub.core.constants.database import DatabaseEdgeType
@@ -342,6 +341,8 @@ class DiffCombiner:
342
341
 
343
342
  def _copy_node_without_parents(self, node: EnrichedDiffNode) -> EnrichedDiffNode:
344
343
  rels_without_parents = {replace(r, nodes=set()) for r in node.relationships}
344
+ for rel in rels_without_parents:
345
+ rel.reset_summaries()
345
346
  node_without_parents = replace(node, relationships=rels_without_parents)
346
347
  return deepcopy(node_without_parents)
347
348
 
@@ -351,15 +352,11 @@ class DiffCombiner:
351
352
  if node_pair.earlier is None:
352
353
  if node_pair.later is not None:
353
354
  copied = self._copy_node_without_parents(node_pair.later)
354
- for rel in copied.relationships:
355
- rel.reset_summaries()
356
355
  combined_nodes.add(copied)
357
356
  continue
358
357
  if node_pair.later is None:
359
358
  if node_pair.earlier is not None:
360
359
  copied = self._copy_node_without_parents(node_pair.earlier)
361
- for rel in copied.relationships:
362
- rel.reset_summaries()
363
360
  combined_nodes.add(copied)
364
361
  continue
365
362
  combined_attributes = self._combine_attributes(
@@ -420,18 +417,25 @@ class DiffCombiner:
420
417
  filtered_node_pairs = self._filter_nodes_to_keep(earlier_diff=earlier, later_diff=later)
421
418
  combined_nodes = self._combine_nodes(node_pairs=filtered_node_pairs)
422
419
  self._link_child_nodes(nodes=combined_nodes)
420
+ if earlier.exists_on_database:
421
+ diff_uuid = earlier.uuid
422
+ partner_uuid = earlier.partner_uuid
423
+ else:
424
+ diff_uuid = later.uuid
425
+ partner_uuid = later.partner_uuid
423
426
  combined_diffs.append(
424
427
  EnrichedDiffRoot(
425
- uuid=str(uuid4()),
426
- partner_uuid=later.partner_uuid,
428
+ uuid=diff_uuid,
429
+ partner_uuid=partner_uuid,
427
430
  base_branch_name=later.base_branch_name,
428
431
  diff_branch_name=later.diff_branch_name,
429
432
  from_time=earlier.from_time,
430
433
  to_time=later.to_time,
434
+ tracking_id=later.tracking_id,
431
435
  nodes=combined_nodes,
432
436
  )
433
437
  )
434
- base_branch_diff, diff_branch_diff = combined_diffs # pylint: disable=unbalanced-tuple-unpacking
438
+ base_branch_diff, diff_branch_diff = combined_diffs
435
439
  base_branch_diff.partner_uuid = diff_branch_diff.uuid
436
440
  diff_branch_diff.partner_uuid = base_branch_diff.uuid
437
441
  return EnrichedDiffs(