infrahub-server 1.2.0rc0__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. infrahub/api/dependencies.py +6 -6
  2. infrahub/api/diff/validation_models.py +7 -7
  3. infrahub/api/schema.py +1 -1
  4. infrahub/artifacts/models.py +5 -3
  5. infrahub/artifacts/tasks.py +3 -5
  6. infrahub/cli/__init__.py +13 -9
  7. infrahub/cli/constants.py +3 -0
  8. infrahub/cli/db.py +165 -183
  9. infrahub/cli/upgrade.py +146 -0
  10. infrahub/computed_attribute/gather.py +185 -0
  11. infrahub/computed_attribute/models.py +240 -12
  12. infrahub/computed_attribute/tasks.py +77 -441
  13. infrahub/computed_attribute/triggers.py +13 -47
  14. infrahub/config.py +43 -32
  15. infrahub/context.py +14 -0
  16. infrahub/core/account.py +4 -4
  17. infrahub/core/attribute.py +58 -58
  18. infrahub/core/branch/tasks.py +74 -22
  19. infrahub/core/changelog/diff.py +95 -36
  20. infrahub/core/changelog/models.py +217 -43
  21. infrahub/core/constants/__init__.py +28 -0
  22. infrahub/core/constants/infrahubkind.py +2 -0
  23. infrahub/core/constants/schema.py +2 -0
  24. infrahub/core/constraint/node/runner.py +9 -8
  25. infrahub/core/diff/branch_differ.py +10 -10
  26. infrahub/core/diff/enricher/cardinality_one.py +5 -0
  27. infrahub/core/diff/enricher/hierarchy.py +17 -4
  28. infrahub/core/diff/enricher/labels.py +5 -0
  29. infrahub/core/diff/enricher/path_identifier.py +4 -0
  30. infrahub/core/diff/ipam_diff_parser.py +4 -5
  31. infrahub/core/diff/model/diff.py +27 -27
  32. infrahub/core/diff/model/path.py +32 -9
  33. infrahub/core/diff/parent_node_adder.py +78 -0
  34. infrahub/core/diff/payload_builder.py +13 -2
  35. infrahub/core/diff/query/filters.py +2 -2
  36. infrahub/core/diff/query/merge.py +20 -17
  37. infrahub/core/diff/query/save.py +188 -182
  38. infrahub/core/diff/query/summary_counts_enricher.py +51 -4
  39. infrahub/core/diff/query_parser.py +4 -4
  40. infrahub/core/diff/repository/deserializer.py +8 -3
  41. infrahub/core/diff/repository/repository.py +156 -38
  42. infrahub/core/diff/tasks.py +4 -4
  43. infrahub/core/graph/__init__.py +1 -1
  44. infrahub/core/graph/index.py +3 -0
  45. infrahub/core/initialization.py +1 -10
  46. infrahub/core/ipam/constants.py +3 -4
  47. infrahub/core/ipam/reconciler.py +12 -12
  48. infrahub/core/ipam/utilization.py +10 -13
  49. infrahub/core/manager.py +36 -36
  50. infrahub/core/merge.py +7 -7
  51. infrahub/core/migrations/__init__.py +2 -3
  52. infrahub/core/migrations/graph/__init__.py +12 -3
  53. infrahub/core/migrations/graph/m017_add_core_profile.py +1 -5
  54. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +4 -4
  55. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +256 -0
  56. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  57. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +51 -0
  58. infrahub/core/migrations/graph/m022_add_generate_template_attr.py +48 -0
  59. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +96 -0
  60. infrahub/core/migrations/query/attribute_add.py +2 -2
  61. infrahub/core/migrations/query/node_duplicate.py +43 -26
  62. infrahub/core/migrations/query/schema_attribute_update.py +2 -2
  63. infrahub/core/migrations/schema/models.py +19 -4
  64. infrahub/core/migrations/schema/node_remove.py +26 -12
  65. infrahub/core/migrations/schema/tasks.py +2 -2
  66. infrahub/core/migrations/shared.py +16 -16
  67. infrahub/core/models.py +15 -6
  68. infrahub/core/node/__init__.py +43 -39
  69. infrahub/core/node/base.py +2 -4
  70. infrahub/core/node/constraints/attribute_uniqueness.py +2 -2
  71. infrahub/core/node/constraints/grouped_uniqueness.py +99 -47
  72. infrahub/core/node/constraints/interface.py +1 -2
  73. infrahub/core/node/delete_validator.py +3 -5
  74. infrahub/core/node/ipam.py +4 -4
  75. infrahub/core/node/permissions.py +7 -7
  76. infrahub/core/node/resource_manager/ip_address_pool.py +6 -6
  77. infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -6
  78. infrahub/core/node/resource_manager/number_pool.py +3 -3
  79. infrahub/core/path.py +12 -12
  80. infrahub/core/property.py +11 -11
  81. infrahub/core/protocols.py +7 -0
  82. infrahub/core/protocols_base.py +21 -21
  83. infrahub/core/query/__init__.py +33 -33
  84. infrahub/core/query/attribute.py +6 -4
  85. infrahub/core/query/diff.py +3 -3
  86. infrahub/core/query/node.py +82 -32
  87. infrahub/core/query/relationship.py +228 -40
  88. infrahub/core/query/resource_manager.py +2 -0
  89. infrahub/core/query/standard_node.py +3 -3
  90. infrahub/core/query/subquery.py +9 -9
  91. infrahub/core/registry.py +13 -15
  92. infrahub/core/relationship/constraints/count.py +3 -4
  93. infrahub/core/relationship/constraints/peer_kind.py +3 -4
  94. infrahub/core/relationship/constraints/profiles_kind.py +2 -2
  95. infrahub/core/relationship/model.py +51 -59
  96. infrahub/core/schema/attribute_schema.py +16 -8
  97. infrahub/core/schema/basenode_schema.py +105 -44
  98. infrahub/core/schema/computed_attribute.py +3 -3
  99. infrahub/core/schema/definitions/core/__init__.py +147 -0
  100. infrahub/core/schema/definitions/core/account.py +171 -0
  101. infrahub/core/schema/definitions/core/artifact.py +136 -0
  102. infrahub/core/schema/definitions/core/builtin.py +24 -0
  103. infrahub/core/schema/definitions/core/check.py +68 -0
  104. infrahub/core/schema/definitions/core/core.py +17 -0
  105. infrahub/core/schema/definitions/core/generator.py +100 -0
  106. infrahub/core/schema/definitions/core/graphql_query.py +79 -0
  107. infrahub/core/schema/definitions/core/group.py +108 -0
  108. infrahub/core/schema/definitions/core/ipam.py +193 -0
  109. infrahub/core/schema/definitions/core/lineage.py +19 -0
  110. infrahub/core/schema/definitions/core/menu.py +48 -0
  111. infrahub/core/schema/definitions/core/permission.py +163 -0
  112. infrahub/core/schema/definitions/core/profile.py +18 -0
  113. infrahub/core/schema/definitions/core/propose_change.py +97 -0
  114. infrahub/core/schema/definitions/core/propose_change_comment.py +193 -0
  115. infrahub/core/schema/definitions/core/propose_change_validator.py +328 -0
  116. infrahub/core/schema/definitions/core/repository.py +286 -0
  117. infrahub/core/schema/definitions/core/resource_pool.py +170 -0
  118. infrahub/core/schema/definitions/core/template.py +27 -0
  119. infrahub/core/schema/definitions/core/transform.py +96 -0
  120. infrahub/core/schema/definitions/core/webhook.py +134 -0
  121. infrahub/core/schema/definitions/internal.py +16 -16
  122. infrahub/core/schema/dropdown.py +3 -4
  123. infrahub/core/schema/generated/attribute_schema.py +15 -18
  124. infrahub/core/schema/generated/base_node_schema.py +12 -14
  125. infrahub/core/schema/generated/node_schema.py +3 -5
  126. infrahub/core/schema/generated/relationship_schema.py +9 -11
  127. infrahub/core/schema/generic_schema.py +2 -2
  128. infrahub/core/schema/manager.py +20 -9
  129. infrahub/core/schema/node_schema.py +4 -2
  130. infrahub/core/schema/relationship_schema.py +14 -6
  131. infrahub/core/schema/schema_branch.py +292 -144
  132. infrahub/core/schema/schema_branch_computed.py +41 -4
  133. infrahub/core/task/task.py +3 -3
  134. infrahub/core/task/user_task.py +15 -15
  135. infrahub/core/timestamp.py +3 -3
  136. infrahub/core/utils.py +20 -18
  137. infrahub/core/validators/__init__.py +1 -3
  138. infrahub/core/validators/aggregated_checker.py +2 -2
  139. infrahub/core/validators/attribute/choices.py +2 -2
  140. infrahub/core/validators/attribute/enum.py +2 -2
  141. infrahub/core/validators/attribute/kind.py +2 -2
  142. infrahub/core/validators/attribute/length.py +2 -2
  143. infrahub/core/validators/attribute/optional.py +2 -2
  144. infrahub/core/validators/attribute/regex.py +2 -2
  145. infrahub/core/validators/attribute/unique.py +2 -2
  146. infrahub/core/validators/checks_runner.py +25 -2
  147. infrahub/core/validators/determiner.py +1 -3
  148. infrahub/core/validators/interface.py +6 -2
  149. infrahub/core/validators/model.py +22 -3
  150. infrahub/core/validators/models/validate_migration.py +17 -4
  151. infrahub/core/validators/node/attribute.py +2 -2
  152. infrahub/core/validators/node/generate_profile.py +2 -2
  153. infrahub/core/validators/node/hierarchy.py +3 -5
  154. infrahub/core/validators/node/inherit_from.py +27 -5
  155. infrahub/core/validators/node/relationship.py +2 -2
  156. infrahub/core/validators/relationship/count.py +4 -4
  157. infrahub/core/validators/relationship/optional.py +2 -2
  158. infrahub/core/validators/relationship/peer.py +2 -2
  159. infrahub/core/validators/shared.py +2 -2
  160. infrahub/core/validators/tasks.py +8 -0
  161. infrahub/core/validators/uniqueness/checker.py +22 -21
  162. infrahub/core/validators/uniqueness/index.py +2 -2
  163. infrahub/core/validators/uniqueness/model.py +11 -11
  164. infrahub/database/__init__.py +27 -22
  165. infrahub/database/metrics.py +7 -1
  166. infrahub/dependencies/builder/constraint/grouped/node_runner.py +1 -3
  167. infrahub/dependencies/builder/diff/deserializer.py +3 -1
  168. infrahub/dependencies/builder/diff/enricher/hierarchy.py +3 -1
  169. infrahub/dependencies/builder/diff/parent_node_adder.py +8 -0
  170. infrahub/dependencies/component/registry.py +2 -2
  171. infrahub/events/__init__.py +25 -2
  172. infrahub/events/artifact_action.py +64 -0
  173. infrahub/events/branch_action.py +33 -22
  174. infrahub/events/generator.py +71 -0
  175. infrahub/events/group_action.py +51 -21
  176. infrahub/events/models.py +18 -19
  177. infrahub/events/node_action.py +88 -37
  178. infrahub/events/repository_action.py +5 -18
  179. infrahub/events/schema_action.py +4 -9
  180. infrahub/events/utils.py +16 -0
  181. infrahub/events/validator_action.py +55 -0
  182. infrahub/exceptions.py +32 -24
  183. infrahub/generators/models.py +2 -3
  184. infrahub/generators/tasks.py +24 -4
  185. infrahub/git/base.py +7 -7
  186. infrahub/git/integrator.py +48 -24
  187. infrahub/git/models.py +101 -9
  188. infrahub/git/repository.py +3 -3
  189. infrahub/git/tasks.py +408 -6
  190. infrahub/git/utils.py +48 -0
  191. infrahub/git/worktree.py +1 -2
  192. infrahub/git_credential/askpass.py +1 -2
  193. infrahub/graphql/analyzer.py +12 -0
  194. infrahub/graphql/app.py +13 -15
  195. infrahub/graphql/context.py +39 -0
  196. infrahub/graphql/initialization.py +3 -0
  197. infrahub/graphql/loaders/node.py +2 -12
  198. infrahub/graphql/loaders/peers.py +77 -0
  199. infrahub/graphql/loaders/shared.py +13 -0
  200. infrahub/graphql/manager.py +17 -19
  201. infrahub/graphql/mutations/artifact_definition.py +5 -5
  202. infrahub/graphql/mutations/branch.py +26 -1
  203. infrahub/graphql/mutations/computed_attribute.py +9 -5
  204. infrahub/graphql/mutations/diff.py +23 -11
  205. infrahub/graphql/mutations/diff_conflict.py +5 -0
  206. infrahub/graphql/mutations/generator.py +83 -0
  207. infrahub/graphql/mutations/graphql_query.py +5 -5
  208. infrahub/graphql/mutations/ipam.py +54 -74
  209. infrahub/graphql/mutations/main.py +195 -132
  210. infrahub/graphql/mutations/menu.py +7 -7
  211. infrahub/graphql/mutations/models.py +2 -4
  212. infrahub/graphql/mutations/node_getter/by_default_filter.py +10 -10
  213. infrahub/graphql/mutations/node_getter/by_hfid.py +1 -3
  214. infrahub/graphql/mutations/node_getter/by_id.py +1 -3
  215. infrahub/graphql/mutations/node_getter/interface.py +1 -2
  216. infrahub/graphql/mutations/proposed_change.py +7 -7
  217. infrahub/graphql/mutations/relationship.py +93 -19
  218. infrahub/graphql/mutations/repository.py +8 -8
  219. infrahub/graphql/mutations/resource_manager.py +3 -3
  220. infrahub/graphql/mutations/schema.py +19 -4
  221. infrahub/graphql/mutations/webhook.py +137 -0
  222. infrahub/graphql/parser.py +4 -4
  223. infrahub/graphql/permissions.py +1 -10
  224. infrahub/graphql/queries/diff/tree.py +19 -14
  225. infrahub/graphql/queries/event.py +5 -2
  226. infrahub/graphql/queries/ipam.py +2 -2
  227. infrahub/graphql/queries/relationship.py +2 -2
  228. infrahub/graphql/queries/search.py +2 -2
  229. infrahub/graphql/resolvers/many_relationship.py +264 -0
  230. infrahub/graphql/resolvers/resolver.py +13 -110
  231. infrahub/graphql/schema.py +2 -0
  232. infrahub/graphql/subscription/graphql_query.py +2 -0
  233. infrahub/graphql/types/context.py +12 -0
  234. infrahub/graphql/types/event.py +84 -17
  235. infrahub/graphql/types/node.py +2 -2
  236. infrahub/graphql/utils.py +2 -2
  237. infrahub/groups/ancestors.py +29 -0
  238. infrahub/groups/parsers.py +107 -0
  239. infrahub/lock.py +20 -20
  240. infrahub/menu/constants.py +0 -1
  241. infrahub/menu/generator.py +9 -21
  242. infrahub/menu/menu.py +17 -38
  243. infrahub/menu/models.py +117 -16
  244. infrahub/menu/repository.py +111 -0
  245. infrahub/menu/utils.py +5 -8
  246. infrahub/message_bus/__init__.py +11 -13
  247. infrahub/message_bus/messages/__init__.py +1 -21
  248. infrahub/message_bus/messages/check_generator_run.py +3 -3
  249. infrahub/message_bus/messages/finalize_validator_execution.py +3 -0
  250. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +6 -0
  251. infrahub/message_bus/messages/request_generatordefinition_check.py +2 -0
  252. infrahub/message_bus/messages/send_echo_request.py +1 -1
  253. infrahub/message_bus/operations/__init__.py +1 -10
  254. infrahub/message_bus/operations/check/__init__.py +2 -2
  255. infrahub/message_bus/operations/check/generator.py +1 -0
  256. infrahub/message_bus/operations/event/__init__.py +2 -2
  257. infrahub/message_bus/operations/event/worker.py +0 -3
  258. infrahub/message_bus/operations/finalize/validator.py +51 -1
  259. infrahub/message_bus/operations/requests/__init__.py +0 -2
  260. infrahub/message_bus/operations/requests/generator_definition.py +21 -23
  261. infrahub/message_bus/operations/requests/proposed_change.py +14 -10
  262. infrahub/permissions/globals.py +15 -0
  263. infrahub/pools/number.py +2 -4
  264. infrahub/proposed_change/models.py +3 -0
  265. infrahub/proposed_change/tasks.py +58 -45
  266. infrahub/pytest_plugin.py +13 -10
  267. infrahub/server.py +2 -3
  268. infrahub/services/__init__.py +2 -2
  269. infrahub/services/adapters/cache/__init__.py +4 -6
  270. infrahub/services/adapters/cache/nats.py +4 -5
  271. infrahub/services/adapters/cache/redis.py +3 -7
  272. infrahub/services/adapters/event/__init__.py +1 -1
  273. infrahub/services/adapters/message_bus/__init__.py +3 -3
  274. infrahub/services/adapters/message_bus/local.py +2 -2
  275. infrahub/services/adapters/message_bus/nats.py +4 -4
  276. infrahub/services/adapters/message_bus/rabbitmq.py +4 -4
  277. infrahub/services/adapters/workflow/local.py +2 -2
  278. infrahub/services/component.py +5 -5
  279. infrahub/services/protocols.py +7 -7
  280. infrahub/services/scheduler.py +1 -3
  281. infrahub/task_manager/event.py +102 -9
  282. infrahub/task_manager/models.py +27 -7
  283. infrahub/tasks/artifact.py +7 -6
  284. infrahub/telemetry/__init__.py +0 -0
  285. infrahub/telemetry/constants.py +9 -0
  286. infrahub/telemetry/database.py +86 -0
  287. infrahub/telemetry/models.py +65 -0
  288. infrahub/telemetry/task_manager.py +77 -0
  289. infrahub/{tasks/telemetry.py → telemetry/tasks.py} +49 -56
  290. infrahub/telemetry/utils.py +11 -0
  291. infrahub/trace.py +4 -4
  292. infrahub/transformations/tasks.py +2 -2
  293. infrahub/trigger/catalogue.py +4 -6
  294. infrahub/trigger/constants.py +0 -8
  295. infrahub/trigger/models.py +54 -5
  296. infrahub/trigger/setup.py +90 -0
  297. infrahub/trigger/tasks.py +35 -84
  298. infrahub/utils.py +11 -1
  299. infrahub/validators/__init__.py +0 -0
  300. infrahub/validators/events.py +42 -0
  301. infrahub/validators/tasks.py +41 -0
  302. infrahub/webhook/gather.py +17 -0
  303. infrahub/webhook/models.py +176 -44
  304. infrahub/webhook/tasks.py +154 -155
  305. infrahub/webhook/triggers.py +31 -7
  306. infrahub/workers/infrahub_async.py +2 -2
  307. infrahub/workers/utils.py +2 -2
  308. infrahub/workflows/catalogue.py +86 -35
  309. infrahub/workflows/initialization.py +8 -2
  310. infrahub/workflows/models.py +27 -1
  311. infrahub/workflows/utils.py +10 -1
  312. infrahub_sdk/client.py +35 -8
  313. infrahub_sdk/config.py +3 -0
  314. infrahub_sdk/context.py +13 -0
  315. infrahub_sdk/ctl/branch.py +3 -2
  316. infrahub_sdk/ctl/cli_commands.py +5 -1
  317. infrahub_sdk/ctl/utils.py +0 -16
  318. infrahub_sdk/exceptions.py +12 -0
  319. infrahub_sdk/generator.py +4 -1
  320. infrahub_sdk/graphql.py +45 -13
  321. infrahub_sdk/node.py +71 -22
  322. infrahub_sdk/protocols.py +21 -8
  323. infrahub_sdk/protocols_base.py +32 -11
  324. infrahub_sdk/query_groups.py +6 -35
  325. infrahub_sdk/schema/__init__.py +55 -26
  326. infrahub_sdk/schema/main.py +8 -0
  327. infrahub_sdk/task/__init__.py +11 -0
  328. infrahub_sdk/task/constants.py +3 -0
  329. infrahub_sdk/task/exceptions.py +25 -0
  330. infrahub_sdk/task/manager.py +551 -0
  331. infrahub_sdk/task/models.py +74 -0
  332. infrahub_sdk/testing/schemas/animal.py +9 -0
  333. infrahub_sdk/timestamp.py +142 -33
  334. infrahub_sdk/utils.py +29 -1
  335. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/METADATA +8 -6
  336. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/RECORD +349 -293
  337. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/entry_points.txt +1 -0
  338. infrahub_testcontainers/constants.py +2 -0
  339. infrahub_testcontainers/container.py +157 -12
  340. infrahub_testcontainers/docker-compose.test.yml +31 -6
  341. infrahub_testcontainers/helpers.py +18 -73
  342. infrahub_testcontainers/host.py +41 -0
  343. infrahub_testcontainers/measurements.py +93 -0
  344. infrahub_testcontainers/models.py +38 -0
  345. infrahub_testcontainers/performance_test.py +166 -0
  346. infrahub_testcontainers/plugin.py +136 -0
  347. infrahub_testcontainers/prometheus.yml +30 -0
  348. infrahub/core/schema/definitions/core.py +0 -2286
  349. infrahub/message_bus/messages/check_repository_checkdefinition.py +0 -20
  350. infrahub/message_bus/messages/check_repository_mergeconflicts.py +0 -16
  351. infrahub/message_bus/messages/check_repository_usercheck.py +0 -26
  352. infrahub/message_bus/messages/event_branch_create.py +0 -11
  353. infrahub/message_bus/messages/event_branch_delete.py +0 -11
  354. infrahub/message_bus/messages/event_branch_rebased.py +0 -9
  355. infrahub/message_bus/messages/event_node_mutated.py +0 -15
  356. infrahub/message_bus/messages/event_schema_update.py +0 -9
  357. infrahub/message_bus/messages/request_repository_checks.py +0 -12
  358. infrahub/message_bus/messages/request_repository_userchecks.py +0 -18
  359. infrahub/message_bus/operations/check/repository.py +0 -293
  360. infrahub/message_bus/operations/event/node.py +0 -20
  361. infrahub/message_bus/operations/event/schema.py +0 -17
  362. infrahub/message_bus/operations/requests/repository.py +0 -133
  363. infrahub/webhook/constants.py +0 -1
  364. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/LICENSE.txt +0 -0
  365. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/WHEEL +0 -0
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  import copy
4
4
  import hashlib
5
5
  from collections import defaultdict
6
- from itertools import chain
7
- from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Union
6
+ from itertools import chain, combinations
7
+ from typing import Any
8
8
 
9
9
  from infrahub_sdk.topological_sort import DependencyCycleExistsError, topological_sort
10
10
  from infrahub_sdk.utils import compare_lists, deep_merge_dict, duplicates, intersection
@@ -13,6 +13,7 @@ from typing_extensions import Self
13
13
  from infrahub.computed_attribute.constants import VALID_KINDS as VALID_COMPUTED_ATTRIBUTE_KINDS
14
14
  from infrahub.core.constants import (
15
15
  OBJECT_TEMPLATE_NAME_ATTR,
16
+ OBJECT_TEMPLATE_RELATIONSHIP_NAME,
16
17
  RESERVED_ATTR_GEN_NAMES,
17
18
  RESERVED_ATTR_REL_NAMES,
18
19
  RESTRICTED_NAMESPACES,
@@ -55,14 +56,13 @@ from infrahub.types import ATTRIBUTE_TYPES
55
56
  from infrahub.utils import format_label
56
57
  from infrahub.visuals import select_color
57
58
 
59
+ from ... import config
60
+ from ..constants.schema import PARENT_CHILD_IDENTIFIER
58
61
  from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
59
62
  from .schema_branch_computed import ComputedAttributes
60
63
 
61
64
  log = get_logger()
62
65
 
63
- if TYPE_CHECKING:
64
- from pydantic import ValidationInfo
65
-
66
66
 
67
67
  class SchemaBranch:
68
68
  def __init__(
@@ -72,7 +72,7 @@ class SchemaBranch:
72
72
  data: dict[str, dict[str, str]] | None = None,
73
73
  computed_attributes: ComputedAttributes | None = None,
74
74
  ):
75
- self._cache: dict[str, Union[NodeSchema, GenericSchema]] = cache
75
+ self._cache: dict[str, NodeSchema | GenericSchema] = cache
76
76
  self.name: str | None = name
77
77
  self.nodes: dict[str, str] = {}
78
78
  self.generics: dict[str, str] = {}
@@ -87,15 +87,11 @@ class SchemaBranch:
87
87
  self.templates = data.get("templates", {})
88
88
 
89
89
  @classmethod
90
- def __get_validators__(cls) -> Iterator[Callable[..., Any]]: # noqa: PLW3201
91
- yield cls.validate
92
-
93
- @classmethod
94
- def validate(cls, v: Any, info: ValidationInfo) -> Self: # noqa: ARG003
95
- if isinstance(v, cls):
96
- return v
97
- if isinstance(v, dict):
98
- return cls.from_dict_schema_object(data=v)
90
+ def validate(cls, data: Any) -> Self: # noqa: ARG003
91
+ if isinstance(data, cls):
92
+ return data
93
+ if isinstance(data, dict):
94
+ return cls.from_dict_schema_object(data=data)
99
95
  raise ValueError("must be a class or a dict")
100
96
 
101
97
  @property
@@ -106,6 +102,10 @@ class SchemaBranch:
106
102
  def generic_names(self) -> list[str]:
107
103
  return list(self.generics.keys())
108
104
 
105
+ @property
106
+ def generic_names_without_templates(self) -> list[str]:
107
+ return [g for g in self.generic_names if not g.startswith("Template")]
108
+
109
109
  @property
110
110
  def profile_names(self) -> list[str]:
111
111
  return list(self.profiles.keys())
@@ -114,10 +114,10 @@ class SchemaBranch:
114
114
  def template_names(self) -> list[str]:
115
115
  return list(self.templates.keys())
116
116
 
117
- def get_all_kind_id_map(self, exclude_profiles: bool = False) -> dict[str, str]:
117
+ def get_all_kind_id_map(self, nodes_and_generics_only: bool = False) -> dict[str, str]:
118
118
  kind_id_map = {}
119
- if exclude_profiles:
120
- names = self.node_names + self.generic_names
119
+ if nodes_and_generics_only:
120
+ names = self.node_names + self.generic_names_without_templates
121
121
  else:
122
122
  names = self.all_names
123
123
  for name in names:
@@ -181,8 +181,8 @@ class SchemaBranch:
181
181
 
182
182
  def diff(self, other: SchemaBranch) -> SchemaDiff:
183
183
  # Identify the nodes or generics that have been added or removed
184
- local_kind_id_map = self.get_all_kind_id_map(exclude_profiles=True)
185
- other_kind_id_map = other.get_all_kind_id_map(exclude_profiles=True)
184
+ local_kind_id_map = self.get_all_kind_id_map(nodes_and_generics_only=True)
185
+ other_kind_id_map = other.get_all_kind_id_map(nodes_and_generics_only=True)
186
186
  clean_local_ids = [id for id in local_kind_id_map.values() if id is not None]
187
187
  clean_other_ids = [id for id in other_kind_id_map.values() if id is not None]
188
188
  shared_ids = intersection(list1=clean_local_ids, list2=clean_other_ids)
@@ -236,12 +236,6 @@ class SchemaBranch:
236
236
  other_item = schema.get(name=item_kind)
237
237
  self.set(name=item_kind, schema=other_item)
238
238
 
239
- # for item_kind in local_only:
240
- # if item_kind in self.nodes:
241
- # del self.nodes[item_kind]
242
- # else:
243
- # del self.generics[item_kind]
244
-
245
239
  def validate_node_deletions(self, diff: SchemaDiff) -> None:
246
240
  """Given a diff, check if a deleted node is still used in relationships of other nodes."""
247
241
  removed_schema_names = set(diff.removed.keys())
@@ -263,7 +257,7 @@ class SchemaBranch:
263
257
  result.validate_all(migration_map=MIGRATION_MAP, validator_map=CONSTRAINT_VALIDATOR_MAP)
264
258
  return result
265
259
 
266
- def duplicate(self, name: Optional[str] = None) -> SchemaBranch:
260
+ def duplicate(self, name: str | None = None) -> SchemaBranch:
267
261
  """Duplicate the current object but conserve the same cache."""
268
262
  return self.__class__(
269
263
  name=name,
@@ -433,7 +427,7 @@ class SchemaBranch:
433
427
  return list(namespaces.values())
434
428
 
435
429
  def get_schemas_for_namespaces(
436
- self, namespaces: Optional[list[str]] = None, include_internal: bool = False
430
+ self, namespaces: list[str] | None = None, include_internal: bool = False
437
431
  ) -> list[MainSchemaTypes]:
438
432
  """Retrive everything in a single dictionary."""
439
433
  all_schemas = self.get_all(include_internal=include_internal, duplicate=False)
@@ -450,12 +444,12 @@ class SchemaBranch:
450
444
  nodes.append(self.get(name=node_name, duplicate=True))
451
445
  return nodes
452
446
 
453
- def generate_fields_for_display_label(self, name: str) -> Optional[dict]:
447
+ def generate_fields_for_display_label(self, name: str) -> dict | None:
454
448
  node = self.get(name=name, duplicate=False)
455
449
  if isinstance(node, NodeSchema | ProfileSchema | TemplateSchema):
456
450
  return node.generate_fields_for_display_label()
457
451
 
458
- fields: dict[str, Union[str, None, dict[str, None]]] = {}
452
+ fields: dict[str, str | None | dict[str, None]] = {}
459
453
  if isinstance(node, GenericSchema):
460
454
  for child_node_name in node.used_by:
461
455
  child_node = self.get(name=child_node_name, duplicate=False)
@@ -620,7 +614,7 @@ class SchemaBranch:
620
614
  node_schema: BaseNodeSchema,
621
615
  path: str,
622
616
  allowed_path_types: SchemaElementPathType,
623
- element_name: Optional[str] = None,
617
+ element_name: str | None = None,
624
618
  ) -> SchemaAttributePath:
625
619
  error_header = f"{node_schema.kind}"
626
620
  error_header += f".{element_name}" if element_name else ""
@@ -686,7 +680,7 @@ class SchemaBranch:
686
680
  return schema_attribute_path
687
681
 
688
682
  def sync_uniqueness_constraints_and_unique_attributes(self) -> None:
689
- for name in self.generic_names + self.node_names:
683
+ for name in self.generic_names_without_templates + self.node_names:
690
684
  node_schema = self.get(name=name, duplicate=False)
691
685
 
692
686
  if not node_schema.unique_attributes and not node_schema.uniqueness_constraints:
@@ -722,10 +716,12 @@ class SchemaBranch:
722
716
  for attr_name in attrs_to_make_unique:
723
717
  attr_schema = node_schema.get_attribute(name=attr_name)
724
718
  attr_schema.unique = True
719
+
725
720
  if attrs_to_add_to_constraints:
726
721
  node_schema.uniqueness_constraints = (node_schema.uniqueness_constraints or []) + sorted(
727
722
  [[f"{attr_name}__value"] for attr_name in attrs_to_add_to_constraints]
728
723
  )
724
+
729
725
  self.set(name=name, schema=node_schema)
730
726
 
731
727
  def validate_uniqueness_constraints(self) -> None:
@@ -801,7 +797,7 @@ class SchemaBranch:
801
797
  )
802
798
 
803
799
  def validate_default_values(self) -> None:
804
- for name in self.generic_names + self.node_names:
800
+ for name in self.generic_names_without_templates + self.node_names:
805
801
  node_schema = self.get(name=name, duplicate=False)
806
802
  for node_attr in node_schema.local_attributes:
807
803
  if node_attr.default_value is None:
@@ -820,15 +816,35 @@ class SchemaBranch:
820
816
  f"{node_schema.namespace}{node_schema.name}: default value {exc.message}"
821
817
  ) from exc
822
818
 
819
+ def _is_attr_combination_unique(
820
+ self, attrs_paths: list[str], uniqueness_constraints: list[list[str]] | None
821
+ ) -> bool:
822
+ """
823
+ Return whether at least one combination of any length of `attrs_paths` is equal to a uniqueness constraint.
824
+ """
825
+
826
+ if not uniqueness_constraints:
827
+ return False
828
+
829
+ unique_constraint_group_sets = [set(ucg) for ucg in uniqueness_constraints]
830
+ for i in range(1, len(attrs_paths) + 1):
831
+ for attr_combo in combinations(attrs_paths, i):
832
+ if any(ucg == set(attr_combo) for ucg in unique_constraint_group_sets):
833
+ return True
834
+ return False
835
+
823
836
  def validate_human_friendly_id(self) -> None:
824
- for name in self.generic_names + self.node_names:
837
+ for name in self.generic_names_without_templates + self.node_names:
825
838
  node_schema = self.get(name=name, duplicate=False)
826
- hf_attr_names = set()
827
839
 
828
840
  if not node_schema.human_friendly_id:
829
841
  continue
830
842
 
831
843
  allowed_types = SchemaElementPathType.ATTR_WITH_PROP | SchemaElementPathType.REL_ONE_MANDATORY_ATTR
844
+
845
+ # Mapping relationship identifiers -> list of attributes paths
846
+ rel_schemas_to_paths: dict[str, tuple[MainSchemaTypes, list[str]]] = {}
847
+
832
848
  for hfid_path in node_schema.human_friendly_id:
833
849
  schema_path = self.validate_schema_path(
834
850
  node_schema=node_schema,
@@ -837,12 +853,24 @@ class SchemaBranch:
837
853
  element_name="human_friendly_id",
838
854
  )
839
855
 
840
- if schema_path.is_type_attribute:
841
- hf_attr_names.add(schema_path.attribute_schema.name)
856
+ if schema_path.is_type_relationship:
857
+ # Construct the name without relationship prefix to match with how it would be defined in peer schema uniqueness constraint
858
+ rel_identifier = schema_path.relationship_schema.identifier
859
+ if rel_identifier not in rel_schemas_to_paths:
860
+ rel_schemas_to_paths[rel_identifier] = (schema_path.related_schema, [])
861
+ rel_schemas_to_paths[rel_identifier][1].append(schema_path.attribute_path_as_str)
862
+
863
+ if config.SETTINGS.main.schema_strict_mode:
864
+ # For every relationship referred within hfid, check whether the combination of attributes is unique is the peer schema node
865
+ for related_schema, attrs_paths in rel_schemas_to_paths.values():
866
+ if not self._is_attr_combination_unique(attrs_paths, related_schema.uniqueness_constraints):
867
+ raise ValidationError(
868
+ f"HFID of {node_schema.kind} refers peer {related_schema.kind} with a non-unique combination of attributes {attrs_paths}"
869
+ )
842
870
 
843
871
  def validate_required_relationships(self) -> None:
844
872
  reverse_dependency_map: dict[str, set[str]] = {}
845
- for name in self.node_names + self.generic_names:
873
+ for name in self.node_names + self.generic_names_without_templates:
846
874
  node_schema = self.get(name=name, duplicate=False)
847
875
  for relationship_schema in node_schema.relationships:
848
876
  if relationship_schema.optional:
@@ -860,7 +888,7 @@ class SchemaBranch:
860
888
  def validate_parent_component(self) -> None:
861
889
  # {parent_kind: {component_kind_1, component_kind_2, ...}}
862
890
  dependency_map: dict[str, set[str]] = defaultdict(set)
863
- for name in self.generic_names + self.node_names:
891
+ for name in self.generic_names_without_templates + self.node_names:
864
892
  node_schema = self.get(name=name, duplicate=False)
865
893
 
866
894
  parent_relationships: list[RelationshipSchema] = []
@@ -899,7 +927,7 @@ class SchemaBranch:
899
927
  raise ValueError(f"Cycles exist among parents and components in schema: {exc.get_cycle_strings()}") from exc
900
928
 
901
929
  def _validate_parents_one_schema(
902
- self, node_schema: Union[NodeSchema, GenericSchema], parent_relationships: list[RelationshipSchema]
930
+ self, node_schema: NodeSchema | GenericSchema, parent_relationships: list[RelationshipSchema]
903
931
  ) -> None:
904
932
  if not parent_relationships:
905
933
  return
@@ -960,9 +988,9 @@ class SchemaBranch:
960
988
  for rel in node.relationships:
961
989
  if rel.peer in [InfrahubKind.GENERICGROUP]:
962
990
  continue
963
- if not self.has(rel.peer):
991
+ if not self.has(rel.peer) or self.get(rel.peer, duplicate=False).state == HashableModelState.ABSENT:
964
992
  raise ValueError(
965
- f"{node.kind}: Relationship {rel.name!r} is referencing an invalid peer {rel.peer!r}"
993
+ f"{node.kind}: Relationship {rel.name!r} is referring an invalid peer {rel.peer!r}"
966
994
  ) from None
967
995
 
968
996
  def validate_computed_attributes(self) -> None:
@@ -1129,7 +1157,7 @@ class SchemaBranch:
1129
1157
  for name in self.all_names:
1130
1158
  node = self.get(name=name, duplicate=False)
1131
1159
 
1132
- schema_to_update: Optional[Union[NodeSchema, GenericSchema]] = None
1160
+ schema_to_update: NodeSchema | GenericSchema | None = None
1133
1161
  for relationship in node.relationships:
1134
1162
  if relationship.on_delete is not None:
1135
1163
  continue
@@ -1146,49 +1174,50 @@ class SchemaBranch:
1146
1174
  self.set(name=schema_to_update.kind, schema=schema_to_update)
1147
1175
 
1148
1176
  def process_human_friendly_id(self) -> None:
1149
- for name in self.generic_names + self.node_names:
1177
+ """
1178
+ For each schema node, if there is no HFID defined, set it with:
1179
+ - The first unique attribute if existing
1180
+ - Otherwise the first uniqueness constraint with a single attribute
1181
+
1182
+ Also, HFID is added to the uniqueness constraints.
1183
+ """
1184
+ for name in self.generic_names_without_templates + self.node_names:
1150
1185
  node = self.get(name=name, duplicate=False)
1151
1186
 
1152
- # If human_friendly_id IS NOT defined
1153
- # but some the model has some unique attribute, we generate a human_friendly_id
1154
- # If human_friendly_id IS defined
1155
- # but no unique attributes and no uniquess constraints, we add a uniqueness_constraint
1156
- if not node.human_friendly_id and node.unique_attributes:
1157
- for attr in node.unique_attributes:
1187
+ if not node.human_friendly_id:
1188
+ if node.unique_attributes:
1158
1189
  node = self.get(name=name, duplicate=True)
1159
- node.human_friendly_id = [f"{attr.name}__value"]
1190
+ node.human_friendly_id = [f"{node.unique_attributes[0].name}__value"]
1160
1191
  self.set(name=node.kind, schema=node)
1161
- break
1162
- continue
1163
1192
 
1164
- # if no human_friendly_id and a uniqueness_constraint with a single attribute exists
1165
- # then use that attribute as the human_friendly_id
1166
- if not node.human_friendly_id and node.uniqueness_constraints:
1167
- for constraint_paths in node.uniqueness_constraints:
1168
- if len(constraint_paths) > 1:
1169
- continue
1170
- constraint_path = constraint_paths[0]
1171
- schema_path = node.parse_schema_path(path=constraint_path, schema=node)
1172
- if (
1173
- schema_path.is_type_attribute
1174
- and schema_path.attribute_property_name == "value"
1175
- and schema_path.attribute_schema
1176
- ):
1177
- node = self.get(name=name, duplicate=True)
1178
- node.human_friendly_id = [f"{schema_path.attribute_schema.name}__value"]
1179
- self.set(name=node.kind, schema=node)
1180
- break
1181
-
1182
- if node.human_friendly_id and not node.uniqueness_constraints:
1183
- uniqueness_constraints: list[str] = []
1184
- for item in node.human_friendly_id:
1185
- schema_attribute_path = node.parse_schema_path(path=item, schema=self)
1186
- if schema_attribute_path.is_type_attribute:
1187
- uniqueness_constraints.append(item)
1188
- elif schema_attribute_path.is_type_relationship:
1189
- uniqueness_constraints.append(schema_attribute_path.relationship_schema.name)
1190
-
1191
- node.uniqueness_constraints = [uniqueness_constraints]
1193
+ # if no human_friendly_id and a uniqueness_constraint with a single attribute exists
1194
+ # then use that attribute as the human_friendly_id
1195
+ elif node.uniqueness_constraints:
1196
+ for constraint_paths in node.uniqueness_constraints:
1197
+ if len(constraint_paths) > 1:
1198
+ continue
1199
+ constraint_path = constraint_paths[0]
1200
+ schema_path = node.parse_schema_path(path=constraint_path, schema=node)
1201
+ if (
1202
+ schema_path.is_type_attribute
1203
+ and schema_path.attribute_property_name == "value"
1204
+ and schema_path.attribute_schema
1205
+ ):
1206
+ node = self.get(name=name, duplicate=True)
1207
+ node.human_friendly_id = [f"{schema_path.attribute_schema.name}__value"]
1208
+ self.set(name=node.kind, schema=node)
1209
+ break
1210
+
1211
+ # Add hfid to uniqueness constraint
1212
+ hfid_uniqueness_constraint = node.convert_hfid_to_uniqueness_constraint(schema_branch=self)
1213
+ if hfid_uniqueness_constraint:
1214
+ node = self.get(name=name, duplicate=True)
1215
+ # Make sure there is no duplicate regarding generics values.
1216
+ if node.uniqueness_constraints:
1217
+ if hfid_uniqueness_constraint not in node.uniqueness_constraints:
1218
+ node.uniqueness_constraints.append(hfid_uniqueness_constraint)
1219
+ else:
1220
+ node.uniqueness_constraints = [hfid_uniqueness_constraint]
1192
1221
  self.set(name=node.kind, schema=node)
1193
1222
 
1194
1223
  def process_hierarchy(self) -> None:
@@ -1208,7 +1237,7 @@ class SchemaBranch:
1208
1237
  node = node.duplicate()
1209
1238
  changed = False
1210
1239
 
1211
- if node.hierarchy not in node.inherit_from:
1240
+ if node.hierarchy and node.hierarchy not in node.inherit_from:
1212
1241
  node.inherit_from.append(node.hierarchy)
1213
1242
  changed = True
1214
1243
 
@@ -1227,6 +1256,23 @@ class SchemaBranch:
1227
1256
  if changed:
1228
1257
  self.set(name=name, schema=node)
1229
1258
 
1259
+ def _get_generic_fields_map(
1260
+ self, node_schema: MainSchemaTypes
1261
+ ) -> dict[str, tuple[GenericSchema, AttributeSchema | RelationshipSchema]]:
1262
+ generic_fields_map: dict[str, tuple[GenericSchema, AttributeSchema | RelationshipSchema]] = {}
1263
+ if isinstance(node_schema, NodeSchema) and node_schema.inherit_from:
1264
+ for generic_kind in node_schema.inherit_from:
1265
+ generic_schema = self.get_generic(name=generic_kind, duplicate=False)
1266
+ for generic_attr in generic_schema.attributes:
1267
+ if generic_attr.name in node_schema.attribute_names:
1268
+ generic_fields_map[generic_attr.name] = (generic_schema, generic_attr)
1269
+ continue
1270
+ for generic_rel in generic_schema.relationships:
1271
+ if generic_rel.name in node_schema.relationship_names:
1272
+ generic_fields_map[generic_rel.name] = (generic_schema, generic_rel)
1273
+ continue
1274
+ return generic_fields_map
1275
+
1230
1276
  def process_inheritance(self) -> None:
1231
1277
  """Extend all the nodes with the attributes and relationships
1232
1278
  from the Interface objects defined in inherited_from.
@@ -1279,7 +1325,7 @@ class SchemaBranch:
1279
1325
 
1280
1326
  # Update all generics with the list of nodes referrencing them.
1281
1327
  for generic_name in self.generics.keys():
1282
- generic = self.get(name=generic_name)
1328
+ generic = self.get_generic(name=generic_name)
1283
1329
 
1284
1330
  if generic.kind in generics_used_by:
1285
1331
  generic.used_by = sorted(generics_used_by[generic.kind])
@@ -1297,40 +1343,46 @@ class SchemaBranch:
1297
1343
  for name in self.all_names:
1298
1344
  node = self.get(name=name, duplicate=False)
1299
1345
 
1300
- # Check if this node requires a change before duplicating
1301
- change_required = False
1302
- for attr in node.attributes:
1303
- if attr.branch is None:
1304
- change_required = True
1305
- break
1306
- if not change_required:
1307
- for rel in node.relationships:
1308
- if rel.branch is None:
1309
- change_required = True
1310
- break
1311
-
1312
- if not change_required:
1313
- continue
1314
-
1315
- node = node.duplicate()
1346
+ generic_fields_map = self._get_generic_fields_map(node_schema=node)
1316
1347
 
1348
+ attrs_to_update: dict[str, BranchSupportType] = {}
1317
1349
  for attr in node.attributes:
1318
- if attr.branch is not None:
1319
- continue
1350
+ if attr.inherited and attr.name in generic_fields_map:
1351
+ generic_schema, generic_attr = generic_fields_map[attr.name]
1352
+ if attr.branch == generic_schema.branch == generic_attr.branch != node.branch:
1353
+ attrs_to_update[attr.name] = node.branch
1320
1354
 
1321
- attr.branch = node.branch
1355
+ if attr.branch is None:
1356
+ attrs_to_update[attr.name] = node.branch
1322
1357
 
1358
+ rels_to_update: dict[str, BranchSupportType] = {}
1323
1359
  for rel in node.relationships:
1324
- if rel.branch is not None:
1360
+ if not rel.inherited and rel.branch is not None:
1325
1361
  continue
1362
+ needs_update = rel.branch is None
1363
+ if needs_update is False and rel.inherited and rel.name in generic_fields_map:
1364
+ generic_schema, generic_rel = generic_fields_map[rel.name]
1365
+ if rel.branch == generic_schema.branch == generic_rel.branch != node.branch:
1366
+ needs_update = True
1367
+ if needs_update:
1368
+ peer_node = self.get(name=rel.peer, duplicate=False)
1369
+ if node.branch == peer_node.branch:
1370
+ rels_to_update[rel.name] = node.branch
1371
+ elif BranchSupportType.LOCAL in (node.branch, peer_node.branch):
1372
+ rels_to_update[rel.name] = BranchSupportType.LOCAL
1373
+ else:
1374
+ rels_to_update[rel.name] = BranchSupportType.AWARE
1375
+
1376
+ if not attrs_to_update and not rels_to_update:
1377
+ continue
1326
1378
 
1327
- peer_node = self.get(name=rel.peer, duplicate=False)
1328
- if node.branch == peer_node.branch:
1329
- rel.branch = node.branch
1330
- elif BranchSupportType.LOCAL in (node.branch, peer_node.branch):
1331
- rel.branch = BranchSupportType.LOCAL
1332
- else:
1333
- rel.branch = BranchSupportType.AWARE
1379
+ node = node.duplicate()
1380
+ for node_attr in node.attributes:
1381
+ if node_attr.name in attrs_to_update:
1382
+ node_attr.branch = attrs_to_update[node_attr.name]
1383
+ for node_rel in node.relationships:
1384
+ if node_rel.name in rels_to_update:
1385
+ node_rel.branch = rels_to_update[node_rel.name]
1334
1386
 
1335
1387
  self.set(name=name, schema=node)
1336
1388
 
@@ -1423,14 +1475,15 @@ class SchemaBranch:
1423
1475
  self.set(name=name, schema=node)
1424
1476
 
1425
1477
  def generate_weight(self) -> None:
1426
- for name in self.all_names:
1478
+ for name in self.generic_names:
1427
1479
  node = self.get(name=name, duplicate=False)
1480
+
1428
1481
  items_to_update = [item for item in node.attributes + node.relationships if not item.order_weight]
1482
+
1429
1483
  if not items_to_update:
1430
1484
  continue
1431
1485
 
1432
1486
  node = node.duplicate()
1433
-
1434
1487
  current_weight = 0
1435
1488
  for item in node.attributes + node.relationships:
1436
1489
  current_weight += 1000
@@ -1439,6 +1492,30 @@ class SchemaBranch:
1439
1492
 
1440
1493
  self.set(name=name, schema=node)
1441
1494
 
1495
+ for name in self.node_names + self.profile_names:
1496
+ node = self.get(name=name, duplicate=False)
1497
+
1498
+ items_to_update = [item for item in node.attributes + node.relationships if not item.order_weight]
1499
+
1500
+ if not items_to_update:
1501
+ continue
1502
+ node = node.duplicate()
1503
+
1504
+ generic_fields_map = self._get_generic_fields_map(node_schema=node)
1505
+
1506
+ current_weight = 0
1507
+ for item in node.attributes + node.relationships:
1508
+ current_weight += 1000
1509
+ if not item.order_weight:
1510
+ if item.inherited:
1511
+ _, generic_field = generic_fields_map[item.name]
1512
+ if generic_field:
1513
+ item.order_weight = generic_field.order_weight
1514
+ if not item.order_weight:
1515
+ item.order_weight = current_weight
1516
+
1517
+ self.set(name=name, schema=node)
1518
+
1442
1519
  def cleanup_inherited_elements(self) -> None:
1443
1520
  for name in self.node_names:
1444
1521
  node = self.get_node(name=name, duplicate=False)
@@ -1535,7 +1612,7 @@ class SchemaBranch:
1535
1612
  def _get_hierarchy_child_rel(self, peer: str, hierarchical: str | None, read_only: bool) -> RelationshipSchema:
1536
1613
  return RelationshipSchema(
1537
1614
  name="children",
1538
- identifier="parent__child",
1615
+ identifier=PARENT_CHILD_IDENTIFIER,
1539
1616
  peer=peer,
1540
1617
  kind=RelationshipKind.HIERARCHY,
1541
1618
  cardinality=RelationshipCardinality.MANY,
@@ -1550,7 +1627,7 @@ class SchemaBranch:
1550
1627
  ) -> RelationshipSchema:
1551
1628
  return RelationshipSchema(
1552
1629
  name="parent",
1553
- identifier="parent__child",
1630
+ identifier=PARENT_CHILD_IDENTIFIER,
1554
1631
  peer=peer,
1555
1632
  kind=RelationshipKind.HIERARCHY,
1556
1633
  cardinality=RelationshipCardinality.ONE,
@@ -1629,11 +1706,10 @@ class SchemaBranch:
1629
1706
  if not self.has(name=InfrahubKind.PROFILE):
1630
1707
  # TODO: This logic is actually only for testing purposes as since 1.0.9 CoreProfile is loaded in db.
1631
1708
  # Ideally, we would remove this and instead load CoreProfile properly within tests.
1632
- core_profile_schema = GenericSchema(**core_profile_schema_definition)
1633
- self.set(name=core_profile_schema.kind, schema=core_profile_schema)
1709
+ self.set(name=core_profile_schema_definition.kind, schema=core_profile_schema_definition)
1634
1710
 
1635
1711
  profile_schema_kinds = set()
1636
- for node_name in self.node_names + self.generic_names:
1712
+ for node_name in self.node_names + self.generic_names_without_templates:
1637
1713
  node = self.get(name=node_name, duplicate=False)
1638
1714
  if (
1639
1715
  node.namespace in RESTRICTED_NAMESPACES
@@ -1789,7 +1865,7 @@ class SchemaBranch:
1789
1865
  continue
1790
1866
 
1791
1867
  template_rel_settings: dict[str, Any] = {
1792
- "name": "object_template",
1868
+ "name": OBJECT_TEMPLATE_RELATIONSHIP_NAME,
1793
1869
  "identifier": "node__objecttemplate",
1794
1870
  "peer": self._get_object_template_kind(node.kind),
1795
1871
  "kind": RelationshipKind.TEMPLATE,
@@ -1799,14 +1875,14 @@ class SchemaBranch:
1799
1875
  }
1800
1876
 
1801
1877
  # Add relationship between node and template
1802
- if "object_template" not in node.relationship_names:
1878
+ if OBJECT_TEMPLATE_RELATIONSHIP_NAME not in node.relationship_names:
1803
1879
  node_schema = self.get(name=node_name, duplicate=True)
1804
1880
 
1805
1881
  node_schema.relationships.append(RelationshipSchema(**template_rel_settings))
1806
1882
  self.set(name=node_name, schema=node_schema)
1807
1883
  else:
1808
1884
  has_changes: bool = False
1809
- rel_template = node.get_relationship(name="object_template")
1885
+ rel_template = node.get_relationship(name=OBJECT_TEMPLATE_RELATIONSHIP_NAME)
1810
1886
  for name, value in template_rel_settings.items():
1811
1887
  if getattr(rel_template, name) != value:
1812
1888
  has_changes = True
@@ -1815,7 +1891,7 @@ class SchemaBranch:
1815
1891
  continue
1816
1892
 
1817
1893
  node_schema = self.get(name=node_name, duplicate=True)
1818
- rel_template = node_schema.get_relationship(name="object_template")
1894
+ rel_template = node_schema.get_relationship(name=OBJECT_TEMPLATE_RELATIONSHIP_NAME)
1819
1895
  for name, value in template_rel_settings.items():
1820
1896
  if getattr(rel_template, name) != value:
1821
1897
  setattr(rel_template, name, value)
@@ -1824,21 +1900,28 @@ class SchemaBranch:
1824
1900
 
1825
1901
  def add_relationships_to_template(self, node: NodeSchema) -> None:
1826
1902
  template_schema = self.get(name=self._get_object_template_kind(node_kind=node.kind), duplicate=False)
1903
+ if template_schema.is_generic_schema:
1904
+ return
1905
+
1906
+ # Remove previous relationships to account for new ones
1907
+ template_schema.relationships = [
1908
+ r for r in template_schema.relationships if r.kind == RelationshipKind.TEMPLATE
1909
+ ]
1910
+ # Tell if the user explicitely requested this template
1911
+ is_autogenerated_subtemplate = node.generate_template is False
1827
1912
 
1828
1913
  for relationship in node.relationships:
1829
- if relationship.peer in [
1830
- InfrahubKind.GENERICGROUP,
1831
- InfrahubKind.PROFILE,
1832
- ] or relationship.kind not in [
1914
+ if relationship.peer in [InfrahubKind.GENERICGROUP, InfrahubKind.PROFILE] or relationship.kind not in [
1833
1915
  RelationshipKind.COMPONENT,
1834
1916
  RelationshipKind.PARENT,
1835
1917
  RelationshipKind.ATTRIBUTE,
1918
+ RelationshipKind.GENERIC,
1836
1919
  ]:
1837
1920
  continue
1838
1921
 
1839
1922
  rel_template_peer = (
1840
1923
  self._get_object_template_kind(node_kind=relationship.peer)
1841
- if relationship.kind != RelationshipKind.ATTRIBUTE
1924
+ if relationship.kind not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC]
1842
1925
  else relationship.peer
1843
1926
  )
1844
1927
  template_schema.relationships.append(
@@ -1846,33 +1929,81 @@ class SchemaBranch:
1846
1929
  name=relationship.name,
1847
1930
  peer=rel_template_peer,
1848
1931
  kind=relationship.kind,
1849
- optional=relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.ATTRIBUTE],
1932
+ optional=relationship.optional
1933
+ if is_autogenerated_subtemplate
1934
+ else relationship.kind != RelationshipKind.PARENT,
1850
1935
  cardinality=relationship.cardinality,
1936
+ direction=relationship.direction,
1851
1937
  branch=relationship.branch,
1852
- identifier=self._generate_identifier_string(template_schema.kind, rel_template_peer),
1938
+ identifier=f"template_{relationship.identifier}"
1939
+ if relationship.identifier
1940
+ else self._generate_identifier_string(template_schema.kind, rel_template_peer),
1853
1941
  min_count=relationship.min_count,
1854
1942
  max_count=relationship.max_count,
1943
+ label=f"{relationship.name} template".title()
1944
+ if relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
1945
+ else relationship.name.title(),
1855
1946
  )
1856
1947
  )
1857
1948
 
1858
- def generate_object_template_from_node(self, node: NodeSchema) -> TemplateSchema:
1859
- core_template_schema = self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
1949
+ if relationship.kind == RelationshipKind.PARENT:
1950
+ template_schema.human_friendly_id = [
1951
+ f"{relationship.name}__template_name__value"
1952
+ ] + template_schema.human_friendly_id
1953
+ template_schema.uniqueness_constraints[0].append(relationship.name)
1954
+
1955
+ def generate_object_template_from_node(
1956
+ self, node: NodeSchema | GenericSchema, need_templates: set[NodeSchema | GenericSchema]
1957
+ ) -> TemplateSchema | GenericSchema:
1958
+ # Tell if the user explicitely requested this template
1959
+ is_autogenerated_subtemplate = node.generate_template is False
1960
+
1961
+ core_template_schema = (
1962
+ self.get(name=InfrahubKind.OBJECTCOMPONENTTEMPLATE, duplicate=False)
1963
+ if is_autogenerated_subtemplate
1964
+ else self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
1965
+ )
1860
1966
  core_name_attr = core_template_schema.get_attribute(name=OBJECT_TEMPLATE_NAME_ATTR)
1861
1967
  template_name_attr = AttributeSchema(
1862
1968
  **core_name_attr.model_dump(exclude=["id", "inherited"]),
1863
1969
  )
1864
1970
  template_name_attr.branch = node.branch
1865
1971
 
1972
+ template: TemplateSchema | GenericSchema
1973
+ need_template_kinds = [n.kind for n in need_templates]
1974
+
1975
+ if node.is_generic_schema:
1976
+ # When needing a template for a generic, we generate an empty shell mostly to make sure that schemas (including the GraphQL one) will
1977
+ # look right. We don't really care about applying inheritance of fields as it was already processed and actual templates will have the
1978
+ # correct attributes and relationships
1979
+ template = GenericSchema(
1980
+ name=node.kind,
1981
+ namespace="Template",
1982
+ label=f"Generic object template {node.label}",
1983
+ description=f"Generic object template for generic {node.kind}",
1984
+ generate_profile=False,
1985
+ branch=node.branch,
1986
+ include_in_menu=False,
1987
+ attributes=[template_name_attr],
1988
+ )
1989
+
1990
+ for used in node.used_by:
1991
+ if used in need_template_kinds:
1992
+ template.used_by.append(self._get_object_template_kind(node_kind=used))
1993
+
1994
+ return template
1995
+
1866
1996
  template = TemplateSchema(
1867
1997
  name=node.kind,
1868
1998
  namespace="Template",
1869
1999
  label=f"Object template {node.label}",
1870
2000
  description=f"Object template for {node.kind}",
1871
2001
  branch=node.branch,
1872
- include_in_menu=True,
2002
+ include_in_menu=False,
1873
2003
  display_labels=["template_name__value"],
1874
- inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.OBJECTTEMPLATE, InfrahubKind.NODE],
1875
2004
  human_friendly_id=["template_name__value"],
2005
+ uniqueness_constraints=[["template_name__value"]],
2006
+ inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind],
1876
2007
  default_filter="template_name__value",
1877
2008
  attributes=[template_name_attr],
1878
2009
  relationships=[
@@ -1887,46 +2018,62 @@ class SchemaBranch:
1887
2018
  ],
1888
2019
  )
1889
2020
 
2021
+ for inherited in node.inherit_from:
2022
+ if inherited in need_template_kinds:
2023
+ template.inherit_from.append(self._get_object_template_kind(node_kind=inherited))
2024
+
1890
2025
  for node_attr in node.attributes:
1891
- if node_attr.unique:
2026
+ if node_attr.unique or node_attr.read_only:
1892
2027
  continue
1893
2028
 
1894
2029
  attr = AttributeSchema(
1895
- optional=True, **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "inherited"])
2030
+ optional=node_attr.optional if is_autogenerated_subtemplate else True,
2031
+ **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "inherited"]),
1896
2032
  )
1897
2033
  template.attributes.append(attr)
1898
2034
 
1899
2035
  return template
1900
2036
 
1901
2037
  def identify_required_object_templates(
1902
- self, node_schema: NodeSchema, identified: set[NodeSchema]
2038
+ self, node_schema: NodeSchema | GenericSchema, identified: set[NodeSchema | GenericSchema]
1903
2039
  ) -> set[NodeSchema]:
1904
2040
  """Identify all templates required to turn a given node into a template."""
1905
- if node_schema in identified:
2041
+ if node_schema in identified or node_schema.state == HashableModelState.ABSENT:
1906
2042
  return identified
1907
2043
 
1908
2044
  identified.add(node_schema)
1909
2045
 
1910
2046
  for relationship in node_schema.relationships:
1911
- if relationship.peer in [
1912
- InfrahubKind.GENERICGROUP,
1913
- InfrahubKind.PROFILE,
1914
- ] or relationship.kind not in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]:
2047
+ if (
2048
+ relationship.peer in [InfrahubKind.GENERICGROUP, InfrahubKind.PROFILE]
2049
+ or (relationship.kind == RelationshipKind.PARENT and node_schema.generate_template)
2050
+ or relationship.kind not in [RelationshipKind.PARENT, RelationshipKind.COMPONENT]
2051
+ ):
1915
2052
  continue
1916
2053
 
1917
2054
  peer_schema = self.get(name=relationship.peer, duplicate=False)
1918
- if not isinstance(peer_schema, NodeSchema) or peer_schema in identified:
2055
+ if not isinstance(peer_schema, NodeSchema | GenericSchema) or peer_schema in identified:
1919
2056
  continue
2057
+ # In a context of a generic, we won't be able to create objects out of it, so any kind of nodes implementing the generic is a valid
2058
+ # option, we therefore need to have a template for each of those nodes
2059
+ if isinstance(peer_schema, GenericSchema) and peer_schema.used_by:
2060
+ if relationship.kind != RelationshipKind.PARENT or not any(
2061
+ u in [i.kind for i in identified] for u in peer_schema.used_by
2062
+ ):
2063
+ for used_by in peer_schema.used_by:
2064
+ identified |= self.identify_required_object_templates(
2065
+ node_schema=self.get(name=used_by, duplicate=False), identified=identified
2066
+ )
1920
2067
 
1921
2068
  identified |= self.identify_required_object_templates(node_schema=peer_schema, identified=identified)
1922
2069
 
1923
2070
  return identified
1924
2071
 
1925
2072
  def manage_object_template_schemas(self) -> None:
1926
- need_templates: set[NodeSchema] = set()
2073
+ need_templates: set[NodeSchema | GenericSchema] = set()
1927
2074
  template_schema_kinds: set[str] = set()
1928
2075
 
1929
- for node_name in self.node_names + self.generic_names:
2076
+ for node_name in self.node_names + self.generic_names_without_templates:
1930
2077
  node = self.get(name=node_name, duplicate=False)
1931
2078
 
1932
2079
  # Delete old object templates if schemas were removed
@@ -1936,6 +2083,7 @@ class SchemaBranch:
1936
2083
  or node.state == HashableModelState.ABSENT
1937
2084
  ):
1938
2085
  try:
2086
+ node.relationships = [r for r in node.relationships if r.name != OBJECT_TEMPLATE_RELATIONSHIP_NAME]
1939
2087
  self.delete(name=self._get_object_template_kind(node_kind=node.kind))
1940
2088
  except SchemaNotFoundError:
1941
2089
  ...
@@ -1945,7 +2093,7 @@ class SchemaBranch:
1945
2093
 
1946
2094
  # Generate templates with their attributes
1947
2095
  for node in need_templates:
1948
- template = self.generate_object_template_from_node(node=node)
2096
+ template = self.generate_object_template_from_node(node=node, need_templates=need_templates)
1949
2097
  self.set(name=template.kind, schema=template)
1950
2098
  template_schema_kinds.add(template.kind)
1951
2099