infrahub-server 1.2.0b1__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 (297) 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 +1 -3
  5. infrahub/artifacts/tasks.py +1 -3
  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 +239 -11
  12. infrahub/computed_attribute/tasks.py +77 -442
  13. infrahub/computed_attribute/triggers.py +11 -45
  14. infrahub/config.py +43 -32
  15. infrahub/context.py +14 -0
  16. infrahub/core/account.py +4 -4
  17. infrahub/core/attribute.py +57 -57
  18. infrahub/core/branch/tasks.py +12 -9
  19. infrahub/core/changelog/diff.py +16 -8
  20. infrahub/core/changelog/models.py +189 -26
  21. infrahub/core/constants/__init__.py +5 -1
  22. infrahub/core/constants/infrahubkind.py +2 -0
  23. infrahub/core/constraint/node/runner.py +9 -8
  24. infrahub/core/diff/branch_differ.py +10 -10
  25. infrahub/core/diff/ipam_diff_parser.py +4 -5
  26. infrahub/core/diff/model/diff.py +27 -27
  27. infrahub/core/diff/model/path.py +3 -3
  28. infrahub/core/diff/query/merge.py +20 -17
  29. infrahub/core/diff/query_parser.py +4 -4
  30. infrahub/core/graph/__init__.py +1 -1
  31. infrahub/core/initialization.py +1 -10
  32. infrahub/core/ipam/constants.py +3 -4
  33. infrahub/core/ipam/reconciler.py +12 -12
  34. infrahub/core/ipam/utilization.py +10 -13
  35. infrahub/core/manager.py +34 -34
  36. infrahub/core/merge.py +7 -7
  37. infrahub/core/migrations/__init__.py +2 -3
  38. infrahub/core/migrations/graph/__init__.py +9 -4
  39. infrahub/core/migrations/graph/m017_add_core_profile.py +1 -5
  40. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +4 -4
  41. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  42. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +51 -0
  43. infrahub/core/migrations/graph/{m020_add_generate_template_attr.py → m022_add_generate_template_attr.py} +3 -3
  44. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +96 -0
  45. infrahub/core/migrations/query/attribute_add.py +2 -2
  46. infrahub/core/migrations/query/node_duplicate.py +18 -21
  47. infrahub/core/migrations/query/schema_attribute_update.py +2 -2
  48. infrahub/core/migrations/schema/models.py +19 -4
  49. infrahub/core/migrations/schema/tasks.py +2 -2
  50. infrahub/core/migrations/shared.py +16 -16
  51. infrahub/core/models.py +15 -6
  52. infrahub/core/node/__init__.py +29 -28
  53. infrahub/core/node/base.py +2 -4
  54. infrahub/core/node/constraints/attribute_uniqueness.py +2 -2
  55. infrahub/core/node/constraints/grouped_uniqueness.py +99 -47
  56. infrahub/core/node/constraints/interface.py +1 -2
  57. infrahub/core/node/delete_validator.py +3 -5
  58. infrahub/core/node/ipam.py +4 -4
  59. infrahub/core/node/permissions.py +7 -7
  60. infrahub/core/node/resource_manager/ip_address_pool.py +6 -6
  61. infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -6
  62. infrahub/core/node/resource_manager/number_pool.py +3 -3
  63. infrahub/core/path.py +12 -12
  64. infrahub/core/property.py +11 -11
  65. infrahub/core/protocols.py +5 -0
  66. infrahub/core/protocols_base.py +21 -21
  67. infrahub/core/query/__init__.py +33 -33
  68. infrahub/core/query/attribute.py +6 -4
  69. infrahub/core/query/diff.py +3 -3
  70. infrahub/core/query/node.py +82 -32
  71. infrahub/core/query/relationship.py +24 -24
  72. infrahub/core/query/resource_manager.py +2 -0
  73. infrahub/core/query/standard_node.py +3 -3
  74. infrahub/core/query/subquery.py +9 -9
  75. infrahub/core/registry.py +13 -15
  76. infrahub/core/relationship/constraints/count.py +3 -4
  77. infrahub/core/relationship/constraints/peer_kind.py +3 -4
  78. infrahub/core/relationship/constraints/profiles_kind.py +2 -2
  79. infrahub/core/relationship/model.py +40 -46
  80. infrahub/core/schema/attribute_schema.py +9 -9
  81. infrahub/core/schema/basenode_schema.py +93 -44
  82. infrahub/core/schema/computed_attribute.py +3 -3
  83. infrahub/core/schema/definitions/core/__init__.py +13 -19
  84. infrahub/core/schema/definitions/core/account.py +151 -148
  85. infrahub/core/schema/definitions/core/artifact.py +122 -113
  86. infrahub/core/schema/definitions/core/builtin.py +19 -16
  87. infrahub/core/schema/definitions/core/check.py +61 -53
  88. infrahub/core/schema/definitions/core/core.py +17 -0
  89. infrahub/core/schema/definitions/core/generator.py +89 -85
  90. infrahub/core/schema/definitions/core/graphql_query.py +72 -70
  91. infrahub/core/schema/definitions/core/group.py +96 -93
  92. infrahub/core/schema/definitions/core/ipam.py +176 -235
  93. infrahub/core/schema/definitions/core/lineage.py +18 -16
  94. infrahub/core/schema/definitions/core/menu.py +42 -40
  95. infrahub/core/schema/definitions/core/permission.py +144 -142
  96. infrahub/core/schema/definitions/core/profile.py +16 -27
  97. infrahub/core/schema/definitions/core/propose_change.py +88 -79
  98. infrahub/core/schema/definitions/core/propose_change_comment.py +170 -165
  99. infrahub/core/schema/definitions/core/propose_change_validator.py +290 -288
  100. infrahub/core/schema/definitions/core/repository.py +231 -225
  101. infrahub/core/schema/definitions/core/resource_pool.py +156 -166
  102. infrahub/core/schema/definitions/core/template.py +27 -12
  103. infrahub/core/schema/definitions/core/transform.py +85 -76
  104. infrahub/core/schema/definitions/core/webhook.py +127 -101
  105. infrahub/core/schema/definitions/internal.py +16 -16
  106. infrahub/core/schema/dropdown.py +3 -4
  107. infrahub/core/schema/generated/attribute_schema.py +15 -18
  108. infrahub/core/schema/generated/base_node_schema.py +12 -14
  109. infrahub/core/schema/generated/node_schema.py +3 -5
  110. infrahub/core/schema/generated/relationship_schema.py +9 -11
  111. infrahub/core/schema/generic_schema.py +2 -2
  112. infrahub/core/schema/manager.py +20 -9
  113. infrahub/core/schema/node_schema.py +4 -2
  114. infrahub/core/schema/relationship_schema.py +7 -7
  115. infrahub/core/schema/schema_branch.py +276 -138
  116. infrahub/core/schema/schema_branch_computed.py +41 -4
  117. infrahub/core/task/task.py +3 -3
  118. infrahub/core/task/user_task.py +15 -15
  119. infrahub/core/utils.py +20 -18
  120. infrahub/core/validators/__init__.py +1 -3
  121. infrahub/core/validators/aggregated_checker.py +2 -2
  122. infrahub/core/validators/attribute/choices.py +2 -2
  123. infrahub/core/validators/attribute/enum.py +2 -2
  124. infrahub/core/validators/attribute/kind.py +2 -2
  125. infrahub/core/validators/attribute/length.py +2 -2
  126. infrahub/core/validators/attribute/optional.py +2 -2
  127. infrahub/core/validators/attribute/regex.py +2 -2
  128. infrahub/core/validators/attribute/unique.py +2 -2
  129. infrahub/core/validators/checks_runner.py +25 -2
  130. infrahub/core/validators/determiner.py +1 -3
  131. infrahub/core/validators/interface.py +6 -2
  132. infrahub/core/validators/model.py +22 -3
  133. infrahub/core/validators/models/validate_migration.py +17 -4
  134. infrahub/core/validators/node/attribute.py +2 -2
  135. infrahub/core/validators/node/generate_profile.py +2 -2
  136. infrahub/core/validators/node/hierarchy.py +3 -5
  137. infrahub/core/validators/node/inherit_from.py +27 -5
  138. infrahub/core/validators/node/relationship.py +2 -2
  139. infrahub/core/validators/relationship/count.py +4 -4
  140. infrahub/core/validators/relationship/optional.py +2 -2
  141. infrahub/core/validators/relationship/peer.py +2 -2
  142. infrahub/core/validators/shared.py +2 -2
  143. infrahub/core/validators/tasks.py +8 -0
  144. infrahub/core/validators/uniqueness/checker.py +22 -21
  145. infrahub/core/validators/uniqueness/index.py +2 -2
  146. infrahub/core/validators/uniqueness/model.py +11 -11
  147. infrahub/database/__init__.py +26 -22
  148. infrahub/database/metrics.py +7 -1
  149. infrahub/dependencies/builder/constraint/grouped/node_runner.py +1 -3
  150. infrahub/dependencies/component/registry.py +2 -2
  151. infrahub/events/__init__.py +25 -2
  152. infrahub/events/artifact_action.py +13 -25
  153. infrahub/events/branch_action.py +26 -18
  154. infrahub/events/generator.py +71 -0
  155. infrahub/events/group_action.py +10 -24
  156. infrahub/events/models.py +10 -16
  157. infrahub/events/node_action.py +87 -32
  158. infrahub/events/repository_action.py +5 -18
  159. infrahub/events/schema_action.py +4 -9
  160. infrahub/events/utils.py +16 -0
  161. infrahub/events/validator_action.py +55 -0
  162. infrahub/exceptions.py +23 -24
  163. infrahub/generators/models.py +1 -3
  164. infrahub/git/base.py +7 -7
  165. infrahub/git/integrator.py +26 -25
  166. infrahub/git/models.py +22 -9
  167. infrahub/git/repository.py +3 -3
  168. infrahub/git/tasks.py +67 -49
  169. infrahub/git/utils.py +48 -0
  170. infrahub/git/worktree.py +1 -2
  171. infrahub/git_credential/askpass.py +1 -2
  172. infrahub/graphql/analyzer.py +12 -0
  173. infrahub/graphql/app.py +13 -15
  174. infrahub/graphql/context.py +6 -0
  175. infrahub/graphql/initialization.py +3 -0
  176. infrahub/graphql/loaders/node.py +2 -12
  177. infrahub/graphql/loaders/peers.py +77 -0
  178. infrahub/graphql/loaders/shared.py +13 -0
  179. infrahub/graphql/manager.py +13 -10
  180. infrahub/graphql/mutations/artifact_definition.py +5 -5
  181. infrahub/graphql/mutations/computed_attribute.py +4 -5
  182. infrahub/graphql/mutations/graphql_query.py +5 -5
  183. infrahub/graphql/mutations/ipam.py +50 -70
  184. infrahub/graphql/mutations/main.py +164 -141
  185. infrahub/graphql/mutations/menu.py +5 -5
  186. infrahub/graphql/mutations/models.py +2 -4
  187. infrahub/graphql/mutations/node_getter/by_default_filter.py +10 -10
  188. infrahub/graphql/mutations/node_getter/by_hfid.py +1 -3
  189. infrahub/graphql/mutations/node_getter/by_id.py +1 -3
  190. infrahub/graphql/mutations/node_getter/interface.py +1 -2
  191. infrahub/graphql/mutations/proposed_change.py +7 -7
  192. infrahub/graphql/mutations/relationship.py +67 -35
  193. infrahub/graphql/mutations/repository.py +8 -8
  194. infrahub/graphql/mutations/resource_manager.py +3 -3
  195. infrahub/graphql/mutations/schema.py +4 -4
  196. infrahub/graphql/mutations/webhook.py +137 -0
  197. infrahub/graphql/parser.py +4 -4
  198. infrahub/graphql/queries/diff/tree.py +4 -4
  199. infrahub/graphql/queries/ipam.py +2 -2
  200. infrahub/graphql/queries/relationship.py +2 -2
  201. infrahub/graphql/queries/search.py +2 -2
  202. infrahub/graphql/resolvers/many_relationship.py +264 -0
  203. infrahub/graphql/resolvers/resolver.py +13 -110
  204. infrahub/graphql/subscription/graphql_query.py +2 -0
  205. infrahub/graphql/types/event.py +20 -11
  206. infrahub/graphql/types/node.py +2 -2
  207. infrahub/graphql/utils.py +2 -2
  208. infrahub/groups/ancestors.py +29 -0
  209. infrahub/groups/parsers.py +107 -0
  210. infrahub/menu/generator.py +7 -7
  211. infrahub/menu/menu.py +0 -10
  212. infrahub/menu/models.py +117 -16
  213. infrahub/menu/repository.py +111 -0
  214. infrahub/menu/utils.py +5 -8
  215. infrahub/message_bus/messages/__init__.py +1 -11
  216. infrahub/message_bus/messages/check_generator_run.py +2 -0
  217. infrahub/message_bus/messages/finalize_validator_execution.py +3 -0
  218. infrahub/message_bus/messages/request_generatordefinition_check.py +2 -0
  219. infrahub/message_bus/operations/__init__.py +0 -2
  220. infrahub/message_bus/operations/check/generator.py +1 -0
  221. infrahub/message_bus/operations/event/__init__.py +2 -2
  222. infrahub/message_bus/operations/finalize/validator.py +51 -1
  223. infrahub/message_bus/operations/requests/generator_definition.py +19 -19
  224. infrahub/message_bus/operations/requests/proposed_change.py +3 -1
  225. infrahub/pools/number.py +2 -4
  226. infrahub/proposed_change/tasks.py +37 -28
  227. infrahub/pytest_plugin.py +13 -10
  228. infrahub/server.py +1 -2
  229. infrahub/services/adapters/event/__init__.py +1 -1
  230. infrahub/task_manager/event.py +23 -9
  231. infrahub/tasks/artifact.py +2 -4
  232. infrahub/telemetry/__init__.py +0 -0
  233. infrahub/telemetry/constants.py +9 -0
  234. infrahub/telemetry/database.py +86 -0
  235. infrahub/telemetry/models.py +65 -0
  236. infrahub/telemetry/task_manager.py +77 -0
  237. infrahub/{tasks/telemetry.py → telemetry/tasks.py} +49 -56
  238. infrahub/telemetry/utils.py +11 -0
  239. infrahub/trace.py +4 -4
  240. infrahub/transformations/tasks.py +2 -2
  241. infrahub/trigger/catalogue.py +2 -5
  242. infrahub/trigger/constants.py +0 -8
  243. infrahub/trigger/models.py +14 -1
  244. infrahub/trigger/setup.py +90 -0
  245. infrahub/trigger/tasks.py +35 -90
  246. infrahub/utils.py +11 -1
  247. infrahub/validators/__init__.py +0 -0
  248. infrahub/validators/events.py +42 -0
  249. infrahub/validators/tasks.py +41 -0
  250. infrahub/webhook/gather.py +17 -0
  251. infrahub/webhook/models.py +22 -5
  252. infrahub/webhook/tasks.py +44 -19
  253. infrahub/webhook/triggers.py +22 -5
  254. infrahub/workers/infrahub_async.py +2 -2
  255. infrahub/workers/utils.py +2 -2
  256. infrahub/workflows/catalogue.py +28 -20
  257. infrahub/workflows/initialization.py +1 -3
  258. infrahub/workflows/models.py +1 -1
  259. infrahub/workflows/utils.py +10 -1
  260. infrahub_sdk/client.py +27 -8
  261. infrahub_sdk/config.py +3 -0
  262. infrahub_sdk/context.py +13 -0
  263. infrahub_sdk/exceptions.py +6 -0
  264. infrahub_sdk/generator.py +4 -1
  265. infrahub_sdk/graphql.py +45 -13
  266. infrahub_sdk/node.py +69 -20
  267. infrahub_sdk/protocols_base.py +32 -11
  268. infrahub_sdk/query_groups.py +6 -35
  269. infrahub_sdk/schema/__init__.py +55 -26
  270. infrahub_sdk/schema/main.py +8 -0
  271. infrahub_sdk/task/__init__.py +10 -0
  272. infrahub_sdk/task/manager.py +12 -6
  273. infrahub_sdk/testing/schemas/animal.py +9 -0
  274. infrahub_sdk/timestamp.py +12 -4
  275. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/METADATA +3 -2
  276. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/RECORD +289 -260
  277. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/entry_points.txt +1 -0
  278. infrahub_testcontainers/constants.py +2 -0
  279. infrahub_testcontainers/container.py +157 -12
  280. infrahub_testcontainers/docker-compose.test.yml +31 -6
  281. infrahub_testcontainers/helpers.py +18 -73
  282. infrahub_testcontainers/host.py +41 -0
  283. infrahub_testcontainers/measurements.py +93 -0
  284. infrahub_testcontainers/models.py +38 -0
  285. infrahub_testcontainers/performance_test.py +166 -0
  286. infrahub_testcontainers/plugin.py +136 -0
  287. infrahub_testcontainers/prometheus.yml +30 -0
  288. infrahub/message_bus/messages/event_branch_create.py +0 -11
  289. infrahub/message_bus/messages/event_branch_delete.py +0 -11
  290. infrahub/message_bus/messages/event_branch_rebased.py +0 -9
  291. infrahub/message_bus/messages/event_node_mutated.py +0 -15
  292. infrahub/message_bus/messages/event_schema_update.py +0 -9
  293. infrahub/message_bus/operations/event/node.py +0 -20
  294. infrahub/message_bus/operations/event/schema.py +0 -17
  295. infrahub/webhook/constants.py +0 -1
  296. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/LICENSE.txt +0 -0
  297. {infrahub_server-1.2.0b1.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,15 +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
58
60
  from ..constants.schema import PARENT_CHILD_IDENTIFIER
59
61
  from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
60
62
  from .schema_branch_computed import ComputedAttributes
61
63
 
62
64
  log = get_logger()
63
65
 
64
- if TYPE_CHECKING:
65
- from pydantic import ValidationInfo
66
-
67
66
 
68
67
  class SchemaBranch:
69
68
  def __init__(
@@ -73,7 +72,7 @@ class SchemaBranch:
73
72
  data: dict[str, dict[str, str]] | None = None,
74
73
  computed_attributes: ComputedAttributes | None = None,
75
74
  ):
76
- self._cache: dict[str, Union[NodeSchema, GenericSchema]] = cache
75
+ self._cache: dict[str, NodeSchema | GenericSchema] = cache
77
76
  self.name: str | None = name
78
77
  self.nodes: dict[str, str] = {}
79
78
  self.generics: dict[str, str] = {}
@@ -88,15 +87,11 @@ class SchemaBranch:
88
87
  self.templates = data.get("templates", {})
89
88
 
90
89
  @classmethod
91
- def __get_validators__(cls) -> Iterator[Callable[..., Any]]: # noqa: PLW3201
92
- yield cls.validate
93
-
94
- @classmethod
95
- def validate(cls, v: Any, info: ValidationInfo) -> Self: # noqa: ARG003
96
- if isinstance(v, cls):
97
- return v
98
- if isinstance(v, dict):
99
- 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)
100
95
  raise ValueError("must be a class or a dict")
101
96
 
102
97
  @property
@@ -107,6 +102,10 @@ class SchemaBranch:
107
102
  def generic_names(self) -> list[str]:
108
103
  return list(self.generics.keys())
109
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
+
110
109
  @property
111
110
  def profile_names(self) -> list[str]:
112
111
  return list(self.profiles.keys())
@@ -115,10 +114,10 @@ class SchemaBranch:
115
114
  def template_names(self) -> list[str]:
116
115
  return list(self.templates.keys())
117
116
 
118
- 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]:
119
118
  kind_id_map = {}
120
- if exclude_profiles:
121
- names = self.node_names + self.generic_names
119
+ if nodes_and_generics_only:
120
+ names = self.node_names + self.generic_names_without_templates
122
121
  else:
123
122
  names = self.all_names
124
123
  for name in names:
@@ -182,8 +181,8 @@ class SchemaBranch:
182
181
 
183
182
  def diff(self, other: SchemaBranch) -> SchemaDiff:
184
183
  # Identify the nodes or generics that have been added or removed
185
- local_kind_id_map = self.get_all_kind_id_map(exclude_profiles=True)
186
- 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)
187
186
  clean_local_ids = [id for id in local_kind_id_map.values() if id is not None]
188
187
  clean_other_ids = [id for id in other_kind_id_map.values() if id is not None]
189
188
  shared_ids = intersection(list1=clean_local_ids, list2=clean_other_ids)
@@ -237,12 +236,6 @@ class SchemaBranch:
237
236
  other_item = schema.get(name=item_kind)
238
237
  self.set(name=item_kind, schema=other_item)
239
238
 
240
- # for item_kind in local_only:
241
- # if item_kind in self.nodes:
242
- # del self.nodes[item_kind]
243
- # else:
244
- # del self.generics[item_kind]
245
-
246
239
  def validate_node_deletions(self, diff: SchemaDiff) -> None:
247
240
  """Given a diff, check if a deleted node is still used in relationships of other nodes."""
248
241
  removed_schema_names = set(diff.removed.keys())
@@ -264,7 +257,7 @@ class SchemaBranch:
264
257
  result.validate_all(migration_map=MIGRATION_MAP, validator_map=CONSTRAINT_VALIDATOR_MAP)
265
258
  return result
266
259
 
267
- def duplicate(self, name: Optional[str] = None) -> SchemaBranch:
260
+ def duplicate(self, name: str | None = None) -> SchemaBranch:
268
261
  """Duplicate the current object but conserve the same cache."""
269
262
  return self.__class__(
270
263
  name=name,
@@ -434,7 +427,7 @@ class SchemaBranch:
434
427
  return list(namespaces.values())
435
428
 
436
429
  def get_schemas_for_namespaces(
437
- self, namespaces: Optional[list[str]] = None, include_internal: bool = False
430
+ self, namespaces: list[str] | None = None, include_internal: bool = False
438
431
  ) -> list[MainSchemaTypes]:
439
432
  """Retrive everything in a single dictionary."""
440
433
  all_schemas = self.get_all(include_internal=include_internal, duplicate=False)
@@ -451,12 +444,12 @@ class SchemaBranch:
451
444
  nodes.append(self.get(name=node_name, duplicate=True))
452
445
  return nodes
453
446
 
454
- def generate_fields_for_display_label(self, name: str) -> Optional[dict]:
447
+ def generate_fields_for_display_label(self, name: str) -> dict | None:
455
448
  node = self.get(name=name, duplicate=False)
456
449
  if isinstance(node, NodeSchema | ProfileSchema | TemplateSchema):
457
450
  return node.generate_fields_for_display_label()
458
451
 
459
- fields: dict[str, Union[str, None, dict[str, None]]] = {}
452
+ fields: dict[str, str | None | dict[str, None]] = {}
460
453
  if isinstance(node, GenericSchema):
461
454
  for child_node_name in node.used_by:
462
455
  child_node = self.get(name=child_node_name, duplicate=False)
@@ -621,7 +614,7 @@ class SchemaBranch:
621
614
  node_schema: BaseNodeSchema,
622
615
  path: str,
623
616
  allowed_path_types: SchemaElementPathType,
624
- element_name: Optional[str] = None,
617
+ element_name: str | None = None,
625
618
  ) -> SchemaAttributePath:
626
619
  error_header = f"{node_schema.kind}"
627
620
  error_header += f".{element_name}" if element_name else ""
@@ -687,7 +680,7 @@ class SchemaBranch:
687
680
  return schema_attribute_path
688
681
 
689
682
  def sync_uniqueness_constraints_and_unique_attributes(self) -> None:
690
- for name in self.generic_names + self.node_names:
683
+ for name in self.generic_names_without_templates + self.node_names:
691
684
  node_schema = self.get(name=name, duplicate=False)
692
685
 
693
686
  if not node_schema.unique_attributes and not node_schema.uniqueness_constraints:
@@ -723,10 +716,12 @@ class SchemaBranch:
723
716
  for attr_name in attrs_to_make_unique:
724
717
  attr_schema = node_schema.get_attribute(name=attr_name)
725
718
  attr_schema.unique = True
719
+
726
720
  if attrs_to_add_to_constraints:
727
721
  node_schema.uniqueness_constraints = (node_schema.uniqueness_constraints or []) + sorted(
728
722
  [[f"{attr_name}__value"] for attr_name in attrs_to_add_to_constraints]
729
723
  )
724
+
730
725
  self.set(name=name, schema=node_schema)
731
726
 
732
727
  def validate_uniqueness_constraints(self) -> None:
@@ -802,7 +797,7 @@ class SchemaBranch:
802
797
  )
803
798
 
804
799
  def validate_default_values(self) -> None:
805
- for name in self.generic_names + self.node_names:
800
+ for name in self.generic_names_without_templates + self.node_names:
806
801
  node_schema = self.get(name=name, duplicate=False)
807
802
  for node_attr in node_schema.local_attributes:
808
803
  if node_attr.default_value is None:
@@ -821,15 +816,35 @@ class SchemaBranch:
821
816
  f"{node_schema.namespace}{node_schema.name}: default value {exc.message}"
822
817
  ) from exc
823
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
+
824
836
  def validate_human_friendly_id(self) -> None:
825
- for name in self.generic_names + self.node_names:
837
+ for name in self.generic_names_without_templates + self.node_names:
826
838
  node_schema = self.get(name=name, duplicate=False)
827
- hf_attr_names = set()
828
839
 
829
840
  if not node_schema.human_friendly_id:
830
841
  continue
831
842
 
832
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
+
833
848
  for hfid_path in node_schema.human_friendly_id:
834
849
  schema_path = self.validate_schema_path(
835
850
  node_schema=node_schema,
@@ -838,12 +853,24 @@ class SchemaBranch:
838
853
  element_name="human_friendly_id",
839
854
  )
840
855
 
841
- if schema_path.is_type_attribute:
842
- 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
+ )
843
870
 
844
871
  def validate_required_relationships(self) -> None:
845
872
  reverse_dependency_map: dict[str, set[str]] = {}
846
- for name in self.node_names + self.generic_names:
873
+ for name in self.node_names + self.generic_names_without_templates:
847
874
  node_schema = self.get(name=name, duplicate=False)
848
875
  for relationship_schema in node_schema.relationships:
849
876
  if relationship_schema.optional:
@@ -861,7 +888,7 @@ class SchemaBranch:
861
888
  def validate_parent_component(self) -> None:
862
889
  # {parent_kind: {component_kind_1, component_kind_2, ...}}
863
890
  dependency_map: dict[str, set[str]] = defaultdict(set)
864
- for name in self.generic_names + self.node_names:
891
+ for name in self.generic_names_without_templates + self.node_names:
865
892
  node_schema = self.get(name=name, duplicate=False)
866
893
 
867
894
  parent_relationships: list[RelationshipSchema] = []
@@ -900,7 +927,7 @@ class SchemaBranch:
900
927
  raise ValueError(f"Cycles exist among parents and components in schema: {exc.get_cycle_strings()}") from exc
901
928
 
902
929
  def _validate_parents_one_schema(
903
- self, node_schema: Union[NodeSchema, GenericSchema], parent_relationships: list[RelationshipSchema]
930
+ self, node_schema: NodeSchema | GenericSchema, parent_relationships: list[RelationshipSchema]
904
931
  ) -> None:
905
932
  if not parent_relationships:
906
933
  return
@@ -961,9 +988,9 @@ class SchemaBranch:
961
988
  for rel in node.relationships:
962
989
  if rel.peer in [InfrahubKind.GENERICGROUP]:
963
990
  continue
964
- if not self.has(rel.peer):
991
+ if not self.has(rel.peer) or self.get(rel.peer, duplicate=False).state == HashableModelState.ABSENT:
965
992
  raise ValueError(
966
- 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}"
967
994
  ) from None
968
995
 
969
996
  def validate_computed_attributes(self) -> None:
@@ -1130,7 +1157,7 @@ class SchemaBranch:
1130
1157
  for name in self.all_names:
1131
1158
  node = self.get(name=name, duplicate=False)
1132
1159
 
1133
- schema_to_update: Optional[Union[NodeSchema, GenericSchema]] = None
1160
+ schema_to_update: NodeSchema | GenericSchema | None = None
1134
1161
  for relationship in node.relationships:
1135
1162
  if relationship.on_delete is not None:
1136
1163
  continue
@@ -1147,49 +1174,50 @@ class SchemaBranch:
1147
1174
  self.set(name=schema_to_update.kind, schema=schema_to_update)
1148
1175
 
1149
1176
  def process_human_friendly_id(self) -> None:
1150
- 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:
1151
1185
  node = self.get(name=name, duplicate=False)
1152
1186
 
1153
- # If human_friendly_id IS NOT defined
1154
- # but some the model has some unique attribute, we generate a human_friendly_id
1155
- # If human_friendly_id IS defined
1156
- # but no unique attributes and no uniquess constraints, we add a uniqueness_constraint
1157
- if not node.human_friendly_id and node.unique_attributes:
1158
- for attr in node.unique_attributes:
1187
+ if not node.human_friendly_id:
1188
+ if node.unique_attributes:
1159
1189
  node = self.get(name=name, duplicate=True)
1160
- node.human_friendly_id = [f"{attr.name}__value"]
1190
+ node.human_friendly_id = [f"{node.unique_attributes[0].name}__value"]
1161
1191
  self.set(name=node.kind, schema=node)
1162
- break
1163
- continue
1164
1192
 
1165
- # if no human_friendly_id and a uniqueness_constraint with a single attribute exists
1166
- # then use that attribute as the human_friendly_id
1167
- if not node.human_friendly_id and node.uniqueness_constraints:
1168
- for constraint_paths in node.uniqueness_constraints:
1169
- if len(constraint_paths) > 1:
1170
- continue
1171
- constraint_path = constraint_paths[0]
1172
- schema_path = node.parse_schema_path(path=constraint_path, schema=node)
1173
- if (
1174
- schema_path.is_type_attribute
1175
- and schema_path.attribute_property_name == "value"
1176
- and schema_path.attribute_schema
1177
- ):
1178
- node = self.get(name=name, duplicate=True)
1179
- node.human_friendly_id = [f"{schema_path.attribute_schema.name}__value"]
1180
- self.set(name=node.kind, schema=node)
1181
- break
1182
-
1183
- if node.human_friendly_id and not node.uniqueness_constraints:
1184
- uniqueness_constraints: list[str] = []
1185
- for item in node.human_friendly_id:
1186
- schema_attribute_path = node.parse_schema_path(path=item, schema=self)
1187
- if schema_attribute_path.is_type_attribute:
1188
- uniqueness_constraints.append(item)
1189
- elif schema_attribute_path.is_type_relationship:
1190
- uniqueness_constraints.append(schema_attribute_path.relationship_schema.name)
1191
-
1192
- 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]
1193
1221
  self.set(name=node.kind, schema=node)
1194
1222
 
1195
1223
  def process_hierarchy(self) -> None:
@@ -1209,7 +1237,7 @@ class SchemaBranch:
1209
1237
  node = node.duplicate()
1210
1238
  changed = False
1211
1239
 
1212
- if node.hierarchy not in node.inherit_from:
1240
+ if node.hierarchy and node.hierarchy not in node.inherit_from:
1213
1241
  node.inherit_from.append(node.hierarchy)
1214
1242
  changed = True
1215
1243
 
@@ -1228,6 +1256,23 @@ class SchemaBranch:
1228
1256
  if changed:
1229
1257
  self.set(name=name, schema=node)
1230
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
+
1231
1276
  def process_inheritance(self) -> None:
1232
1277
  """Extend all the nodes with the attributes and relationships
1233
1278
  from the Interface objects defined in inherited_from.
@@ -1280,7 +1325,7 @@ class SchemaBranch:
1280
1325
 
1281
1326
  # Update all generics with the list of nodes referrencing them.
1282
1327
  for generic_name in self.generics.keys():
1283
- generic = self.get(name=generic_name)
1328
+ generic = self.get_generic(name=generic_name)
1284
1329
 
1285
1330
  if generic.kind in generics_used_by:
1286
1331
  generic.used_by = sorted(generics_used_by[generic.kind])
@@ -1298,40 +1343,46 @@ class SchemaBranch:
1298
1343
  for name in self.all_names:
1299
1344
  node = self.get(name=name, duplicate=False)
1300
1345
 
1301
- # Check if this node requires a change before duplicating
1302
- change_required = False
1303
- for attr in node.attributes:
1304
- if attr.branch is None:
1305
- change_required = True
1306
- break
1307
- if not change_required:
1308
- for rel in node.relationships:
1309
- if rel.branch is None:
1310
- change_required = True
1311
- break
1312
-
1313
- if not change_required:
1314
- continue
1315
-
1316
- node = node.duplicate()
1346
+ generic_fields_map = self._get_generic_fields_map(node_schema=node)
1317
1347
 
1348
+ attrs_to_update: dict[str, BranchSupportType] = {}
1318
1349
  for attr in node.attributes:
1319
- if attr.branch is not None:
1320
- 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
1321
1354
 
1322
- attr.branch = node.branch
1355
+ if attr.branch is None:
1356
+ attrs_to_update[attr.name] = node.branch
1323
1357
 
1358
+ rels_to_update: dict[str, BranchSupportType] = {}
1324
1359
  for rel in node.relationships:
1325
- if rel.branch is not None:
1360
+ if not rel.inherited and rel.branch is not None:
1326
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
1327
1378
 
1328
- peer_node = self.get(name=rel.peer, duplicate=False)
1329
- if node.branch == peer_node.branch:
1330
- rel.branch = node.branch
1331
- elif BranchSupportType.LOCAL in (node.branch, peer_node.branch):
1332
- rel.branch = BranchSupportType.LOCAL
1333
- else:
1334
- 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]
1335
1386
 
1336
1387
  self.set(name=name, schema=node)
1337
1388
 
@@ -1424,14 +1475,15 @@ class SchemaBranch:
1424
1475
  self.set(name=name, schema=node)
1425
1476
 
1426
1477
  def generate_weight(self) -> None:
1427
- for name in self.all_names:
1478
+ for name in self.generic_names:
1428
1479
  node = self.get(name=name, duplicate=False)
1480
+
1429
1481
  items_to_update = [item for item in node.attributes + node.relationships if not item.order_weight]
1482
+
1430
1483
  if not items_to_update:
1431
1484
  continue
1432
1485
 
1433
1486
  node = node.duplicate()
1434
-
1435
1487
  current_weight = 0
1436
1488
  for item in node.attributes + node.relationships:
1437
1489
  current_weight += 1000
@@ -1440,6 +1492,30 @@ class SchemaBranch:
1440
1492
 
1441
1493
  self.set(name=name, schema=node)
1442
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
+
1443
1519
  def cleanup_inherited_elements(self) -> None:
1444
1520
  for name in self.node_names:
1445
1521
  node = self.get_node(name=name, duplicate=False)
@@ -1630,11 +1706,10 @@ class SchemaBranch:
1630
1706
  if not self.has(name=InfrahubKind.PROFILE):
1631
1707
  # TODO: This logic is actually only for testing purposes as since 1.0.9 CoreProfile is loaded in db.
1632
1708
  # Ideally, we would remove this and instead load CoreProfile properly within tests.
1633
- core_profile_schema = GenericSchema(**core_profile_schema_definition)
1634
- 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)
1635
1710
 
1636
1711
  profile_schema_kinds = set()
1637
- for node_name in self.node_names + self.generic_names:
1712
+ for node_name in self.node_names + self.generic_names_without_templates:
1638
1713
  node = self.get(name=node_name, duplicate=False)
1639
1714
  if (
1640
1715
  node.namespace in RESTRICTED_NAMESPACES
@@ -1790,7 +1865,7 @@ class SchemaBranch:
1790
1865
  continue
1791
1866
 
1792
1867
  template_rel_settings: dict[str, Any] = {
1793
- "name": "object_template",
1868
+ "name": OBJECT_TEMPLATE_RELATIONSHIP_NAME,
1794
1869
  "identifier": "node__objecttemplate",
1795
1870
  "peer": self._get_object_template_kind(node.kind),
1796
1871
  "kind": RelationshipKind.TEMPLATE,
@@ -1800,14 +1875,14 @@ class SchemaBranch:
1800
1875
  }
1801
1876
 
1802
1877
  # Add relationship between node and template
1803
- if "object_template" not in node.relationship_names:
1878
+ if OBJECT_TEMPLATE_RELATIONSHIP_NAME not in node.relationship_names:
1804
1879
  node_schema = self.get(name=node_name, duplicate=True)
1805
1880
 
1806
1881
  node_schema.relationships.append(RelationshipSchema(**template_rel_settings))
1807
1882
  self.set(name=node_name, schema=node_schema)
1808
1883
  else:
1809
1884
  has_changes: bool = False
1810
- rel_template = node.get_relationship(name="object_template")
1885
+ rel_template = node.get_relationship(name=OBJECT_TEMPLATE_RELATIONSHIP_NAME)
1811
1886
  for name, value in template_rel_settings.items():
1812
1887
  if getattr(rel_template, name) != value:
1813
1888
  has_changes = True
@@ -1816,7 +1891,7 @@ class SchemaBranch:
1816
1891
  continue
1817
1892
 
1818
1893
  node_schema = self.get(name=node_name, duplicate=True)
1819
- rel_template = node_schema.get_relationship(name="object_template")
1894
+ rel_template = node_schema.get_relationship(name=OBJECT_TEMPLATE_RELATIONSHIP_NAME)
1820
1895
  for name, value in template_rel_settings.items():
1821
1896
  if getattr(rel_template, name) != value:
1822
1897
  setattr(rel_template, name, value)
@@ -1825,16 +1900,18 @@ class SchemaBranch:
1825
1900
 
1826
1901
  def add_relationships_to_template(self, node: NodeSchema) -> None:
1827
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
+
1828
1906
  # Remove previous relationships to account for new ones
1829
1907
  template_schema.relationships = [
1830
1908
  r for r in template_schema.relationships if r.kind == RelationshipKind.TEMPLATE
1831
1909
  ]
1910
+ # Tell if the user explicitely requested this template
1911
+ is_autogenerated_subtemplate = node.generate_template is False
1832
1912
 
1833
1913
  for relationship in node.relationships:
1834
- if relationship.peer in [
1835
- InfrahubKind.GENERICGROUP,
1836
- InfrahubKind.PROFILE,
1837
- ] or relationship.kind not in [
1914
+ if relationship.peer in [InfrahubKind.GENERICGROUP, InfrahubKind.PROFILE] or relationship.kind not in [
1838
1915
  RelationshipKind.COMPONENT,
1839
1916
  RelationshipKind.PARENT,
1840
1917
  RelationshipKind.ATTRIBUTE,
@@ -1852,11 +1929,15 @@ class SchemaBranch:
1852
1929
  name=relationship.name,
1853
1930
  peer=rel_template_peer,
1854
1931
  kind=relationship.kind,
1855
- optional=relationship.kind
1856
- in [RelationshipKind.COMPONENT, RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC],
1932
+ optional=relationship.optional
1933
+ if is_autogenerated_subtemplate
1934
+ else relationship.kind != RelationshipKind.PARENT,
1857
1935
  cardinality=relationship.cardinality,
1936
+ direction=relationship.direction,
1858
1937
  branch=relationship.branch,
1859
- 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),
1860
1941
  min_count=relationship.min_count,
1861
1942
  max_count=relationship.max_count,
1862
1943
  label=f"{relationship.name} template".title()
@@ -1865,14 +1946,53 @@ class SchemaBranch:
1865
1946
  )
1866
1947
  )
1867
1948
 
1868
- def generate_object_template_from_node(self, node: NodeSchema) -> TemplateSchema:
1869
- 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
+ )
1870
1966
  core_name_attr = core_template_schema.get_attribute(name=OBJECT_TEMPLATE_NAME_ATTR)
1871
1967
  template_name_attr = AttributeSchema(
1872
1968
  **core_name_attr.model_dump(exclude=["id", "inherited"]),
1873
1969
  )
1874
1970
  template_name_attr.branch = node.branch
1875
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
+
1876
1996
  template = TemplateSchema(
1877
1997
  name=node.kind,
1878
1998
  namespace="Template",
@@ -1881,8 +2001,9 @@ class SchemaBranch:
1881
2001
  branch=node.branch,
1882
2002
  include_in_menu=False,
1883
2003
  display_labels=["template_name__value"],
1884
- inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.OBJECTTEMPLATE, InfrahubKind.NODE],
1885
2004
  human_friendly_id=["template_name__value"],
2005
+ uniqueness_constraints=[["template_name__value"]],
2006
+ inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind],
1886
2007
  default_filter="template_name__value",
1887
2008
  attributes=[template_name_attr],
1888
2009
  relationships=[
@@ -1897,12 +2018,17 @@ class SchemaBranch:
1897
2018
  ],
1898
2019
  )
1899
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
+
1900
2025
  for node_attr in node.attributes:
1901
- if node_attr.unique:
2026
+ if node_attr.unique or node_attr.read_only:
1902
2027
  continue
1903
2028
 
1904
2029
  attr = AttributeSchema(
1905
- 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"]),
1906
2032
  )
1907
2033
  template.attributes.append(attr)
1908
2034
 
@@ -1912,21 +2038,32 @@ class SchemaBranch:
1912
2038
  self, node_schema: NodeSchema | GenericSchema, identified: set[NodeSchema | GenericSchema]
1913
2039
  ) -> set[NodeSchema]:
1914
2040
  """Identify all templates required to turn a given node into a template."""
1915
- if node_schema in identified:
2041
+ if node_schema in identified or node_schema.state == HashableModelState.ABSENT:
1916
2042
  return identified
1917
2043
 
1918
2044
  identified.add(node_schema)
1919
2045
 
1920
2046
  for relationship in node_schema.relationships:
1921
- if relationship.peer in [
1922
- InfrahubKind.GENERICGROUP,
1923
- InfrahubKind.PROFILE,
1924
- ] 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
+ ):
1925
2052
  continue
1926
2053
 
1927
2054
  peer_schema = self.get(name=relationship.peer, duplicate=False)
1928
2055
  if not isinstance(peer_schema, NodeSchema | GenericSchema) or peer_schema in identified:
1929
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
+ )
1930
2067
 
1931
2068
  identified |= self.identify_required_object_templates(node_schema=peer_schema, identified=identified)
1932
2069
 
@@ -1936,7 +2073,7 @@ class SchemaBranch:
1936
2073
  need_templates: set[NodeSchema | GenericSchema] = set()
1937
2074
  template_schema_kinds: set[str] = set()
1938
2075
 
1939
- for node_name in self.node_names + self.generic_names:
2076
+ for node_name in self.node_names + self.generic_names_without_templates:
1940
2077
  node = self.get(name=node_name, duplicate=False)
1941
2078
 
1942
2079
  # Delete old object templates if schemas were removed
@@ -1946,6 +2083,7 @@ class SchemaBranch:
1946
2083
  or node.state == HashableModelState.ABSENT
1947
2084
  ):
1948
2085
  try:
2086
+ node.relationships = [r for r in node.relationships if r.name != OBJECT_TEMPLATE_RELATIONSHIP_NAME]
1949
2087
  self.delete(name=self._get_object_template_kind(node_kind=node.kind))
1950
2088
  except SchemaNotFoundError:
1951
2089
  ...
@@ -1955,7 +2093,7 @@ class SchemaBranch:
1955
2093
 
1956
2094
  # Generate templates with their attributes
1957
2095
  for node in need_templates:
1958
- template = self.generate_object_template_from_node(node=node)
2096
+ template = self.generate_object_template_from_node(node=node, need_templates=need_templates)
1959
2097
  self.set(name=template.kind, schema=template)
1960
2098
  template_schema_kinds.add(template.kind)
1961
2099