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
@@ -10,7 +10,7 @@ from prefect.logging import get_run_logger
10
10
  from infrahub.core.branch import Branch # noqa: TC001
11
11
  from infrahub.core.migrations import MIGRATION_MAP
12
12
  from infrahub.core.path import SchemaPath # noqa: TC001
13
- from infrahub.services import services
13
+ from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
14
14
  from infrahub.workflows.utils import add_branch_tag
15
15
 
16
16
  from .models import SchemaApplyMigrationData, SchemaMigrationPathResponseData
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
20
20
 
21
21
 
22
22
  @flow(name="schema_apply_migrations", flow_run_name="Apply schema migrations", persist_result=True)
23
- async def schema_apply_migrations(message: SchemaApplyMigrationData) -> list[str]:
23
+ async def schema_apply_migrations(message: SchemaApplyMigrationData, service: InfrahubServices) -> list[str]:
24
24
  await add_branch_tag(branch_name=message.branch.name)
25
25
  log = get_run_logger()
26
26
 
@@ -34,7 +34,6 @@ async def schema_apply_migrations(message: SchemaApplyMigrationData) -> list[str
34
34
  log.info(f"Preparing migration for {migration.migration_name!r} ({migration.routing_key})")
35
35
 
36
36
  new_node_schema: Optional[MainSchemaTypes] = None
37
- previous_node_schema: Optional[MainSchemaTypes] = None
38
37
 
39
38
  if message.new_schema.has(name=migration.path.schema_kind):
40
39
  new_node_schema = message.new_schema.get(name=migration.path.schema_kind)
@@ -56,6 +55,7 @@ async def schema_apply_migrations(message: SchemaApplyMigrationData) -> list[str
56
55
  new_node_schema=new_node_schema,
57
56
  previous_node_schema=previous_node_schema,
58
57
  schema_path=migration.path,
58
+ service=service,
59
59
  )
60
60
 
61
61
  async for _, result in batch.execute():
@@ -64,7 +64,7 @@ async def schema_apply_migrations(message: SchemaApplyMigrationData) -> list[str
64
64
  return error_messages
65
65
 
66
66
 
67
- @task(
67
+ @task( # type: ignore[arg-type]
68
68
  name="schema-path-migrate",
69
69
  task_run_name="Migrate Schema Path {migration_name} on {branch.name}",
70
70
  description="Apply a given migration to the database",
@@ -75,10 +75,10 @@ async def schema_path_migrate(
75
75
  branch: Branch,
76
76
  migration_name: str,
77
77
  schema_path: SchemaPath,
78
+ service: InfrahubServices,
78
79
  new_node_schema: MainSchemaTypes | None = None,
79
80
  previous_node_schema: MainSchemaTypes | None = None,
80
81
  ) -> SchemaMigrationPathResponseData:
81
- service = services.service
82
82
  log = get_run_logger()
83
83
 
84
84
  async with service.database.start_session() as db:
@@ -58,7 +58,7 @@ class SchemaMigration(BaseModel):
58
58
  query = await migration_query.init(db=ts, branch=branch, at=at, migration=self)
59
59
  await query.execute(db=ts)
60
60
  result.nbr_migrations_executed += query.get_nbr_migrations_executed()
61
- except Exception as exc: # pylint: disable=broad-exception-caught
61
+ except Exception as exc:
62
62
  result.errors.append(str(exc))
63
63
  return result
64
64
 
@@ -126,7 +126,7 @@ class GraphMigration(BaseModel):
126
126
  try:
127
127
  query = await migration_query.init(db=ts)
128
128
  await query.execute(db=ts)
129
- except Exception as exc: # pylint: disable=broad-exception-caught
129
+ except Exception as exc:
130
130
  result.errors.append(str(exc))
131
131
  return result
132
132
 
@@ -141,7 +141,7 @@ class InternalSchemaMigration(BaseModel):
141
141
 
142
142
  @staticmethod
143
143
  def get_internal_schema() -> SchemaBranch:
144
- from infrahub.core.schema.schema_branch import SchemaBranch # pylint: disable=import-outside-toplevel
144
+ from infrahub.core.schema.schema_branch import SchemaBranch
145
145
 
146
146
  # load the internal schema from
147
147
  schema = SchemaRoot(**internal_schema)
@@ -167,7 +167,7 @@ class InternalSchemaMigration(BaseModel):
167
167
  try:
168
168
  execution_result = await migration.execute(db=db, branch=default_branch)
169
169
  result.errors.extend(execution_result.errors)
170
- except Exception as exc: # pylint: disable=broad-exception-caught
170
+ except Exception as exc:
171
171
  result.errors.append(str(exc))
172
172
  return result
173
173
 
infrahub/core/models.py CHANGED
@@ -105,7 +105,6 @@ class SchemaDiff(BaseModel):
105
105
 
106
106
  indent_str = " " * indentation
107
107
 
108
- # pylint: disable=too-many-nested-blocks
109
108
  for node_action, node_info in data.items():
110
109
  for node_name, elements in node_info.items():
111
110
  print(f"{str(node_name).ljust(column_size)} | {str(node_action).title()}")
@@ -152,7 +151,7 @@ class SchemaUpdateConstraintInfo(BaseModel):
152
151
  return "schema.validator.path"
153
152
 
154
153
  def __hash__(self) -> int:
155
- return hash((type(self),) + tuple([self.constraint_name + self.path.get_path()]))
154
+ return hash((type(self),) + tuple(self.constraint_name + self.path.get_path()))
156
155
 
157
156
 
158
157
  class SchemaUpdateValidationResult(BaseModel):
@@ -170,7 +169,7 @@ class SchemaUpdateValidationResult(BaseModel):
170
169
  return obj
171
170
 
172
171
  def process_diff(self, schema: SchemaBranch) -> None:
173
- for schema_name, schema_diff in self.diff.removed.items():
172
+ for schema_name in self.diff.removed.keys():
174
173
  self.migrations.append(
175
174
  SchemaUpdateMigrationInfo(
176
175
  path=SchemaPath( # type: ignore[call-arg]
@@ -331,7 +330,7 @@ class SchemaUpdateValidationResult(BaseModel):
331
330
 
332
331
  def add_validator_for_migration(self, validator_map: dict[str, Any]) -> None:
333
332
  for migration in self.migrations:
334
- if validator_map.get(migration.migration_name, None):
333
+ if validator_map.get(migration.migration_name):
335
334
  self.constraints.append(
336
335
  SchemaUpdateConstraintInfo(
337
336
  path=migration.path,
@@ -384,7 +383,7 @@ class HashableModel(BaseModel):
384
383
  md5hash.update(item)
385
384
 
386
385
  if display_values:
387
- from rich import print as rprint # pylint: disable=import-outside-toplevel
386
+ from rich import print as rprint
388
387
 
389
388
  rprint(tuple(values))
390
389
 
@@ -494,8 +493,8 @@ class HashableModel(BaseModel):
494
493
  raise ValueError(f"Unable to merge the list for {field_name}, some items have the same _sorting_id")
495
494
 
496
495
  shared_ids = intersection(list(local_sub_items.keys()), list(other_sub_items.keys()))
497
- local_only_ids = set(list(local_sub_items.keys())) - set(shared_ids)
498
- other_only_ids = set(list(other_sub_items.keys())) - set(shared_ids)
496
+ local_only_ids = set(local_sub_items.keys()) - set(shared_ids)
497
+ other_only_ids = set(other_sub_items.keys()) - set(shared_ids)
499
498
 
500
499
  new_list = [value for key, value in local_sub_items.items() if key in local_only_ids]
501
500
  new_list.extend(
@@ -536,7 +535,7 @@ class HashableModel(BaseModel):
536
535
  if attr_other is None or attr_local == attr_other:
537
536
  continue
538
537
 
539
- if attr_local is None or isinstance(attr_other, (int, str, bool, float)):
538
+ if attr_local is None or isinstance(attr_other, int | str | bool | float):
540
539
  setattr(self, field_name, attr_other)
541
540
  continue
542
541
 
@@ -1,17 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import Enum
4
- from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, overload
4
+ from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeVar, Union, overload
5
5
 
6
6
  from infrahub_sdk.utils import is_valid_uuid
7
7
  from infrahub_sdk.uuidt import UUIDT
8
8
 
9
9
  from infrahub.core import registry
10
- from infrahub.core.constants import BranchSupportType, ComputedAttributeKind, InfrahubKind, RelationshipCardinality
10
+ from infrahub.core.changelog.models import NodeChangelog
11
+ from infrahub.core.constants import (
12
+ OBJECT_TEMPLATE_NAME_ATTR,
13
+ OBJECT_TEMPLATE_RELATIONSHIP_NAME,
14
+ BranchSupportType,
15
+ ComputedAttributeKind,
16
+ InfrahubKind,
17
+ RelationshipCardinality,
18
+ RelationshipKind,
19
+ )
11
20
  from infrahub.core.constants.schema import SchemaElementPathType
12
- from infrahub.core.protocols import CoreNumberPool
21
+ from infrahub.core.protocols import CoreNumberPool, CoreObjectTemplate
13
22
  from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
14
- from infrahub.core.schema import AttributeSchema, NodeSchema, ProfileSchema, RelationshipSchema
23
+ from infrahub.core.schema import AttributeSchema, NodeSchema, ProfileSchema, RelationshipSchema, TemplateSchema
15
24
  from infrahub.core.timestamp import Timestamp
16
25
  from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
17
26
  from infrahub.support.macro import MacroDefinition
@@ -42,21 +51,17 @@ SchemaProtocol = TypeVar("SchemaProtocol")
42
51
  # -
43
52
  # ---------------------------------------------------------------------------------------
44
53
 
45
- # pylint: disable=redefined-builtin,too-many-branches
46
-
47
54
 
48
55
  class Node(BaseNode, metaclass=BaseNodeMeta):
49
56
  @classmethod
50
- def __init_subclass_with_meta__( # pylint: disable=arguments-differ
51
- cls, _meta=None, default_filter=None, **options
52
- ) -> None:
57
+ def __init_subclass_with_meta__(cls, _meta=None, default_filter=None, **options) -> None:
53
58
  if not _meta:
54
59
  _meta = BaseNodeOptions(cls)
55
60
 
56
61
  _meta.default_filter = default_filter
57
62
  super().__init_subclass_with_meta__(_meta=_meta, **options)
58
63
 
59
- def get_schema(self) -> Union[NodeSchema, ProfileSchema]:
64
+ def get_schema(self) -> Union[NodeSchema, ProfileSchema, TemplateSchema]:
60
65
  return self._schema
61
66
 
62
67
  def get_kind(self) -> str:
@@ -130,7 +135,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
130
135
  labels.append(InfrahubKind.NODE)
131
136
  return labels
132
137
 
133
- if isinstance(self._schema, ProfileSchema):
138
+ if isinstance(self._schema, ProfileSchema | TemplateSchema):
134
139
  labels = [self.get_kind()] + self._schema.inherit_from
135
140
  return labels
136
141
 
@@ -153,8 +158,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
153
158
 
154
159
  return f"{self.get_kind()}(ID: {str(self.id)})"
155
160
 
156
- def __init__(self, schema: Union[NodeSchema, ProfileSchema], branch: Branch, at: Timestamp):
157
- self._schema: Union[NodeSchema, ProfileSchema] = schema
161
+ def __init__(self, schema: Union[NodeSchema, ProfileSchema, TemplateSchema], branch: Branch, at: Timestamp):
162
+ self._schema: Union[NodeSchema, ProfileSchema, TemplateSchema] = schema
158
163
  self._branch: Branch = branch
159
164
  self._at: Timestamp = at
160
165
  self._existing: bool = False
@@ -171,12 +176,20 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
171
176
  # Lists of attributes and relationships names
172
177
  self._attributes: list[str] = []
173
178
  self._relationships: list[str] = []
179
+ self._node_changelog: NodeChangelog | None = None
180
+
181
+ @property
182
+ def node_changelog(self) -> NodeChangelog:
183
+ if self._node_changelog:
184
+ return self._node_changelog
185
+
186
+ raise InitializationError("The node has not been saved so no changelog exists")
174
187
 
175
188
  @overload
176
189
  @classmethod
177
190
  async def init(
178
191
  cls,
179
- schema: Union[NodeSchema, ProfileSchema, str],
192
+ schema: Union[NodeSchema, ProfileSchema, TemplateSchema, str],
180
193
  db: InfrahubDatabase,
181
194
  branch: Optional[Union[Branch, str]] = ...,
182
195
  at: Optional[Union[Timestamp, str]] = ...,
@@ -195,7 +208,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
195
208
  @classmethod
196
209
  async def init(
197
210
  cls,
198
- schema: Union[NodeSchema, ProfileSchema, str, type[SchemaProtocol]],
211
+ schema: Union[NodeSchema, ProfileSchema, TemplateSchema, str, type[SchemaProtocol]],
199
212
  db: InfrahubDatabase,
200
213
  branch: Optional[Union[Branch, str]] = None,
201
214
  at: Optional[Union[Timestamp, str]] = None,
@@ -204,15 +217,17 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
204
217
 
205
218
  branch = await registry.get_branch(branch=branch, db=db)
206
219
 
207
- if isinstance(schema, (NodeSchema, ProfileSchema)):
220
+ if isinstance(schema, NodeSchema | ProfileSchema | TemplateSchema):
208
221
  attrs["schema"] = schema
209
222
  elif isinstance(schema, str):
210
223
  # TODO need to raise a proper exception for this, right now it will raise a generic ValueError
211
224
  attrs["schema"] = db.schema.get(name=schema, branch=branch)
212
- elif hasattr(schema, "_is_runtime_protocol") and getattr(schema, "_is_runtime_protocol"):
225
+ elif hasattr(schema, "_is_runtime_protocol") and schema._is_runtime_protocol:
213
226
  attrs["schema"] = db.schema.get(name=schema.__name__, branch=branch)
214
227
  else:
215
- raise ValueError(f"Invalid schema provided {type(schema)}, expected NodeSchema or ProfileSchema")
228
+ raise ValueError(
229
+ f"Invalid schema provided {type(schema)}, expected NodeSchema, ProfileSchema or TemplateSchema"
230
+ )
216
231
 
217
232
  attrs["branch"] = branch
218
233
  attrs["at"] = Timestamp(at)
@@ -261,6 +276,56 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
261
276
  )
262
277
  )
263
278
 
279
+ async def handle_object_template(self, fields: dict, db: InfrahubDatabase, errors: list) -> None:
280
+ """Fill the `fields` parameters with values from an object template if one is in use."""
281
+ object_template_field = fields.get(OBJECT_TEMPLATE_RELATIONSHIP_NAME)
282
+ if not object_template_field:
283
+ return
284
+
285
+ try:
286
+ template: CoreObjectTemplate = await registry.manager.find_object(
287
+ db=db,
288
+ kind=self._schema.get_relationship(name=OBJECT_TEMPLATE_RELATIONSHIP_NAME).peer,
289
+ id=object_template_field.get("id"),
290
+ hfid=object_template_field.get("hfid"),
291
+ branch=self.get_branch_based_on_support_type(),
292
+ )
293
+ except NodeNotFoundError:
294
+ errors.append(
295
+ ValidationError(
296
+ {
297
+ f"{OBJECT_TEMPLATE_RELATIONSHIP_NAME}": (
298
+ "Unable to find the object template in the database "
299
+ f"'{object_template_field.get('id') or object_template_field.get('hfid')}'"
300
+ )
301
+ }
302
+ )
303
+ )
304
+ return
305
+
306
+ # Handle attributes, copy values from template
307
+ # Relationships handling in performed in GraphQL mutation to create nodes for relationships
308
+ for attribute_name in template._attributes:
309
+ if attribute_name in list(fields) + [OBJECT_TEMPLATE_NAME_ATTR]:
310
+ continue
311
+ fields[attribute_name] = {"value": getattr(template, attribute_name).value}
312
+
313
+ for relationship_name in template._relationships:
314
+ relationship_schema = template._schema.get_relationship(name=relationship_name)
315
+ if (
316
+ relationship_name in list(fields)
317
+ or relationship_schema.kind != RelationshipKind.ATTRIBUTE
318
+ or relationship_name == OBJECT_TEMPLATE_RELATIONSHIP_NAME
319
+ ):
320
+ continue
321
+
322
+ relationship: RelationshipManager = getattr(template, relationship_name)
323
+ if relationship_schema.cardinality == RelationshipCardinality.ONE:
324
+ if relationship_peer := await relationship.get_peer(db=db):
325
+ fields[relationship_name] = {"id": relationship_peer.id}
326
+ elif relationship_peers := await relationship.get_peers(db=db):
327
+ fields[relationship_name] = [{"id": peer_id} for peer_id in relationship_peers]
328
+
264
329
  async def _process_fields(self, fields: dict, db: InfrahubDatabase) -> None:
265
330
  errors = []
266
331
 
@@ -279,6 +344,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
279
344
  if field_name not in self._schema.valid_input_names:
280
345
  errors.append(ValidationError({field_name: f"{field_name} is not a valid input for {self.get_kind()}"}))
281
346
 
347
+ # Backfill fields with the ones from the template if there's one
348
+ await self.handle_object_template(fields=fields, db=db, errors=errors)
349
+
282
350
  # If the object is new, we need to ensure that all mandatory attributes and relationships have been provided
283
351
  if not self._existing:
284
352
  for mandatory_attr in self._schema.mandatory_attribute_names:
@@ -440,7 +508,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
440
508
 
441
509
  async def _generate_relationship_default(
442
510
  self,
443
- name: str, # pylint: disable=unused-argument
511
+ name: str, # noqa: ARG002
444
512
  schema: RelationshipSchema,
445
513
  data: Any,
446
514
  db: InfrahubDatabase,
@@ -461,7 +529,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
461
529
  name: str,
462
530
  schema: AttributeSchema,
463
531
  data: Any,
464
- db: InfrahubDatabase, # pylint: disable=unused-argument
532
+ db: InfrahubDatabase, # noqa: ARG002
465
533
  ) -> BaseAttribute:
466
534
  attr_class = ATTRIBUTE_TYPES[schema.kind].get_infrahub_class()
467
535
  attr = attr_class(
@@ -476,10 +544,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
476
544
  )
477
545
  return attr
478
546
 
479
- async def process_label(self, db: Optional[InfrahubDatabase] = None) -> None: # pylint: disable=unused-argument
547
+ async def process_label(self, db: Optional[InfrahubDatabase] = None) -> None: # noqa: ARG002
480
548
  # If there label and name are both defined for this node
481
549
  # if label is not define, we'll automatically populate it with a human friendy vesion of name
482
- # pylint: disable=no-member
483
550
  if not self._existing and hasattr(self, "label") and hasattr(self, "name"):
484
551
  if self.label.value is None and self.name.value:
485
552
  self.label.value = " ".join([word.title() for word in self.name.value.split("_")])
@@ -526,7 +593,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
526
593
  await self._process_fields(db=db, fields=kwargs)
527
594
  return self
528
595
 
529
- async def _create(self, db: InfrahubDatabase, at: Optional[Timestamp] = None) -> None:
596
+ async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> NodeChangelog:
530
597
  create_at = Timestamp(at)
531
598
 
532
599
  query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at)
@@ -538,44 +605,63 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
538
605
  self._existing = True
539
606
 
540
607
  new_ids = query.get_ids()
608
+ node_changelog = NodeChangelog(node_id=self.get_id(), node_kind=self.get_kind(), display_label="")
541
609
 
542
610
  # Go over the list of Attribute and assign the new IDs one by one
543
611
  for name in self._attributes:
544
612
  attr: BaseAttribute = getattr(self, name)
545
613
  attr.id, attr.db_id = new_ids[name]
546
614
  attr.at = create_at
615
+ node_changelog.create_attribute(attribute=attr)
547
616
 
548
617
  # Go over the list of relationships and assign the new IDs one by one
549
618
  for name in self._relationships:
550
619
  relm: RelationshipManager = getattr(self, name)
551
620
  for rel in relm._relationships:
552
621
  identifier = f"{rel.schema.identifier}::{rel.peer_id}"
622
+
553
623
  rel.id, rel.db_id = new_ids[identifier]
554
624
 
625
+ node_changelog.create_relationship(relationship=rel)
626
+
627
+ node_changelog.display_label = await self.render_display_label(db=db)
628
+ return node_changelog
629
+
555
630
  async def _update(
556
631
  self, db: InfrahubDatabase, at: Optional[Timestamp] = None, fields: list[str] | None = None
557
- ) -> None:
632
+ ) -> NodeChangelog:
558
633
  """Update the node in the database if needed."""
559
634
 
560
635
  update_at = Timestamp(at)
636
+ node_changelog = NodeChangelog(node_id=self.get_id(), node_kind=self.get_kind(), display_label="")
561
637
 
562
638
  # Go over the list of Attribute and update them one by one
563
639
  for name in self._attributes:
564
- if fields and name in fields:
565
- attr: BaseAttribute = getattr(self, name)
566
- await attr.save(at=update_at, db=db)
567
- else:
640
+ if (fields and name in fields) or not fields:
568
641
  attr: BaseAttribute = getattr(self, name)
569
- await attr.save(at=update_at, db=db)
642
+ updated_attribute = await attr.save(at=update_at, db=db)
643
+ if updated_attribute:
644
+ node_changelog.add_attribute(attribute=updated_attribute)
570
645
 
571
646
  # Go over the list of relationships and update them one by one
647
+ processed_relationships: list[str] = []
572
648
  for name in self._relationships:
573
- if fields and name in fields:
649
+ if (fields and name in fields) or not fields:
650
+ processed_relationships.append(name)
574
651
  rel: RelationshipManager = getattr(self, name)
575
- await rel.save(at=update_at, db=db)
576
- else:
577
- attr: BaseAttribute = getattr(self, name)
578
- await attr.save(at=update_at, db=db)
652
+ updated_relationship = await rel.save(at=update_at, db=db)
653
+ node_changelog.add_relationship(relationship=updated_relationship)
654
+
655
+ if len(processed_relationships) != len(self._relationships):
656
+ # Analyze if the node has a parent and add it to the changelog if missing
657
+ if parent_relationship := self._get_parent_relationship_name():
658
+ if parent_relationship not in processed_relationships:
659
+ rel: RelationshipManager = getattr(self, parent_relationship)
660
+ if parent := await rel.get_parent(db=db):
661
+ node_changelog.add_parent_from_relationship(parent=parent)
662
+
663
+ node_changelog.display_label = await self.render_display_label(db=db)
664
+ return node_changelog
579
665
 
580
666
  async def save(self, db: InfrahubDatabase, at: Optional[Timestamp] = None, fields: list[str] | None = None) -> Self:
581
667
  """Create or Update the Node in the database."""
@@ -583,10 +669,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
583
669
  save_at = Timestamp(at)
584
670
 
585
671
  if self._existing:
586
- await self._update(at=save_at, db=db, fields=fields)
672
+ self._node_changelog = await self._update(at=save_at, db=db, fields=fields)
587
673
  return self
588
674
 
589
- await self._create(at=save_at, db=db)
675
+ self._node_changelog = await self._create(at=save_at, db=db)
590
676
  return self
591
677
 
592
678
  async def delete(self, db: InfrahubDatabase, at: Optional[Timestamp] = None) -> None:
@@ -594,15 +680,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
594
680
 
595
681
  delete_at = Timestamp(at)
596
682
 
683
+ node_changelog = NodeChangelog(
684
+ node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.render_display_label(db=db)
685
+ )
597
686
  # Go over the list of Attribute and update them one by one
598
687
  for name in self._attributes:
599
688
  attr: BaseAttribute = getattr(self, name)
600
- await attr.delete(at=delete_at, db=db)
689
+ deleted_attribute = await attr.delete(at=delete_at, db=db)
690
+ if deleted_attribute:
691
+ node_changelog.add_attribute(attribute=deleted_attribute)
601
692
 
602
693
  # Go over the list of relationships and update them one by one
603
694
  for name in self._relationships:
604
695
  rel: RelationshipManager = getattr(self, name)
605
- await rel.delete(at=delete_at, db=db)
696
+ updated_relationship = await rel.delete(at=delete_at, db=db)
697
+ node_changelog.add_relationship(relationship=updated_relationship)
606
698
 
607
699
  # Need to check if there are some unidirectional relationship as well
608
700
  # For example, if we delete a tag, we must check the permissions and update all the relationships pointing at it
@@ -625,6 +717,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
625
717
 
626
718
  query = await NodeDeleteQuery.init(db=db, node=self, at=delete_at)
627
719
  await query.execute(db=db)
720
+ self._node_changelog = node_changelog
628
721
 
629
722
  async def to_graphql(
630
723
  self,
@@ -633,6 +726,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
633
726
  related_node_ids: set | None = None,
634
727
  filter_sensitive: bool = False,
635
728
  permissions: dict | None = None,
729
+ include_properties: bool = True,
636
730
  ) -> dict:
637
731
  """Generate GraphQL Payload for all attributes
638
732
 
@@ -686,10 +780,14 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
686
780
  related_node_ids=related_node_ids,
687
781
  filter_sensitive=filter_sensitive,
688
782
  permissions=permissions,
783
+ include_properties=include_properties,
689
784
  )
690
785
  else:
691
786
  response[field_name] = await field.to_graphql(
692
- db=db, filter_sensitive=filter_sensitive, permissions=permissions
787
+ db=db,
788
+ filter_sensitive=filter_sensitive,
789
+ permissions=permissions,
790
+ include_properties=include_properties,
693
791
  )
694
792
 
695
793
  for relationship_schema in self.get_schema().relationships:
@@ -731,7 +829,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
731
829
 
732
830
  return changed
733
831
 
734
- async def render_display_label(self, db: Optional[InfrahubDatabase] = None) -> str: # pylint: disable=unused-argument
832
+ async def render_display_label(self, db: Optional[InfrahubDatabase] = None) -> str: # noqa: ARG002
735
833
  if not self._schema.display_labels:
736
834
  return repr(self)
737
835
 
@@ -757,3 +855,26 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
757
855
  if not display_label.strip():
758
856
  return repr(self)
759
857
  return display_label.strip()
858
+
859
+ def _get_parent_relationship_name(self) -> str | None:
860
+ """Return the name of the parent relationship is one is present"""
861
+ for relationship in self._schema.relationships:
862
+ if relationship.kind == RelationshipKind.PARENT:
863
+ return relationship.name
864
+
865
+ async def get_object_template(self, db: InfrahubDatabase) -> Node | None:
866
+ object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
867
+ return await object_template.get_peer(db=db) if object_template is not None else None
868
+
869
+ def get_relationships(
870
+ self, kind: RelationshipKind, exclude: Sequence[str] | None = None
871
+ ) -> list[RelationshipSchema]:
872
+ """Return relationships of a given kind with the possiblity to exclude some of them by name."""
873
+ if exclude is None:
874
+ exclude = []
875
+
876
+ return [
877
+ relationship
878
+ for relationship in self.get_schema().relationships
879
+ if relationship.name not in exclude and relationship.kind == kind
880
+ ]
@@ -21,7 +21,7 @@ class BaseOptions:
21
21
  if not self._frozen:
22
22
  super().__setattr__(name, value)
23
23
  else:
24
- raise Exception(f"Can't modify frozen Options {self}") # pylint: disable=broad-exception-raised
24
+ raise Exception(f"Can't modify frozen Options {self}")
25
25
 
26
26
  def __repr__(self):
27
27
  return f"<{self.__class__.__name__} name={repr(self.name)}>"
@@ -35,7 +35,7 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
35
35
  self.branch = branch
36
36
  self.schema_branch = registry.schema.get_schema_branch(branch.name)
37
37
 
38
- def _build_query_request(
38
+ async def _build_query_request(
39
39
  self,
40
40
  updated_node: Node,
41
41
  node_schema: MainSchemaTypes,
@@ -51,9 +51,16 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
51
51
  if attribute_path.related_schema and attribute_path.relationship_schema:
52
52
  if filters and attribute_path.relationship_schema.name in filters:
53
53
  include_in_query = True
54
+
55
+ relationship_manager: RelationshipManager = getattr(
56
+ updated_node, attribute_path.relationship_schema.name
57
+ )
58
+ related_node = await relationship_manager.get_peer(db=self.db)
59
+ related_node_id = related_node.get_id() if related_node else None
54
60
  query_relationship_paths.add(
55
61
  QueryRelationshipAttributePath(
56
62
  identifier=attribute_path.relationship_schema.get_identifier(),
63
+ value=related_node_id,
57
64
  )
58
65
  )
59
66
  continue
@@ -158,7 +165,7 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
158
165
  ) -> None:
159
166
  schema_branch = self.db.schema.get_schema_branch(name=self.branch.name)
160
167
  path_groups = node_schema.get_unique_constraint_schema_attribute_paths(schema_branch=schema_branch)
161
- query_request = self._build_query_request(
168
+ query_request = await self._build_query_request(
162
169
  updated_node=node, node_schema=node_schema, path_groups=path_groups, filters=filters
163
170
  )
164
171
  if not query_request:
@@ -11,7 +11,7 @@ from infrahub.core.query.relationship import (
11
11
  RelationshipGetByIdentifierQuery,
12
12
  RelationshipPeersData,
13
13
  )
14
- from infrahub.core.schema import MainSchemaTypes, NodeSchema, ProfileSchema
14
+ from infrahub.core.schema import MainSchemaTypes, NodeSchema, ProfileSchema, TemplateSchema
15
15
  from infrahub.core.timestamp import Timestamp
16
16
  from infrahub.database import InfrahubDatabase
17
17
  from infrahub.exceptions import ValidationError
@@ -28,7 +28,7 @@ class NodeDeleteIndex:
28
28
  # {node_schema: {DeleteRelationshipType: {relationship_identifier: peer_node_schema}}}
29
29
  self._dependency_graph: dict[str, dict[DeleteRelationshipType, dict[str, set[str]]]] = {}
30
30
 
31
- def index(self, start_schemas: Iterable[NodeSchema | ProfileSchema]) -> None:
31
+ def index(self, start_schemas: Iterable[NodeSchema | ProfileSchema | TemplateSchema]) -> None:
32
32
  self._index_cascading_deletes(start_schemas=start_schemas)
33
33
  self._index_dependent_schema(start_schemas=start_schemas)
34
34
 
@@ -50,7 +50,7 @@ class NodeDeleteIndex:
50
50
  self._dependency_graph[kind][relationship_type] = defaultdict(set)
51
51
  self._dependency_graph[kind][relationship_type][relationship_identifier].update(peer_kinds)
52
52
 
53
- def _index_cascading_deletes(self, start_schemas: Iterable[NodeSchema | ProfileSchema]) -> None:
53
+ def _index_cascading_deletes(self, start_schemas: Iterable[NodeSchema | ProfileSchema | TemplateSchema]) -> None:
54
54
  kinds_to_check: set[str] = {schema.kind for schema in start_schemas}
55
55
  while True:
56
56
  try:
@@ -72,7 +72,7 @@ class NodeDeleteIndex:
72
72
  if peer_kind not in self._dependency_graph:
73
73
  kinds_to_check.add(peer_kind)
74
74
 
75
- def _index_dependent_schema(self, start_schemas: Iterable[NodeSchema | ProfileSchema]) -> None:
75
+ def _index_dependent_schema(self, start_schemas: Iterable[NodeSchema | ProfileSchema | TemplateSchema]) -> None:
76
76
  start_schema_kinds: set[str] = set()
77
77
  for start_schema in start_schemas:
78
78
  start_schema_kinds.add(start_schema.kind)