infrahub-server 1.5.0b0__py3-none-any.whl → 1.5.0b2__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 (183) hide show
  1. infrahub/actions/tasks.py +8 -0
  2. infrahub/api/diff/diff.py +1 -1
  3. infrahub/api/internal.py +2 -0
  4. infrahub/api/oauth2.py +13 -19
  5. infrahub/api/oidc.py +15 -21
  6. infrahub/api/schema.py +24 -3
  7. infrahub/artifacts/models.py +2 -1
  8. infrahub/auth.py +137 -3
  9. infrahub/cli/__init__.py +2 -0
  10. infrahub/cli/db.py +103 -98
  11. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  12. infrahub/cli/dev.py +118 -0
  13. infrahub/cli/tasks.py +46 -0
  14. infrahub/cli/upgrade.py +30 -3
  15. infrahub/computed_attribute/tasks.py +20 -8
  16. infrahub/core/attribute.py +13 -5
  17. infrahub/core/branch/enums.py +1 -1
  18. infrahub/core/branch/models.py +7 -3
  19. infrahub/core/branch/tasks.py +70 -8
  20. infrahub/core/changelog/models.py +4 -12
  21. infrahub/core/constants/__init__.py +3 -0
  22. infrahub/core/constants/infrahubkind.py +1 -0
  23. infrahub/core/diff/model/path.py +4 -0
  24. infrahub/core/diff/payload_builder.py +1 -1
  25. infrahub/core/diff/query/artifact.py +1 -0
  26. infrahub/core/diff/query/field_summary.py +1 -0
  27. infrahub/core/graph/__init__.py +1 -1
  28. infrahub/core/initialization.py +5 -2
  29. infrahub/core/ipam/utilization.py +1 -1
  30. infrahub/core/manager.py +6 -3
  31. infrahub/core/migrations/__init__.py +3 -0
  32. infrahub/core/migrations/exceptions.py +4 -0
  33. infrahub/core/migrations/graph/__init__.py +12 -11
  34. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  35. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  36. infrahub/core/migrations/graph/m040_duplicated_attributes.py +81 -0
  37. infrahub/core/migrations/graph/m041_profile_attrs_in_db.py +145 -0
  38. infrahub/core/migrations/graph/m042_create_hfid_display_label_in_db.py +164 -0
  39. infrahub/core/migrations/graph/m043_backfill_hfid_display_label_in_db.py +866 -0
  40. infrahub/core/migrations/query/__init__.py +7 -8
  41. infrahub/core/migrations/query/attribute_add.py +8 -6
  42. infrahub/core/migrations/query/attribute_remove.py +134 -0
  43. infrahub/core/migrations/runner.py +54 -0
  44. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  45. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  46. infrahub/core/migrations/schema/node_attribute_add.py +35 -4
  47. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  48. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  49. infrahub/core/migrations/schema/node_remove.py +2 -1
  50. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  51. infrahub/core/migrations/shared.py +52 -19
  52. infrahub/core/node/__init__.py +158 -51
  53. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  54. infrahub/core/node/create.py +46 -63
  55. infrahub/core/node/lock_utils.py +70 -44
  56. infrahub/core/node/node_property_attribute.py +230 -0
  57. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  58. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  59. infrahub/core/node/resource_manager/number_pool.py +2 -1
  60. infrahub/core/node/standard.py +1 -1
  61. infrahub/core/protocols.py +7 -1
  62. infrahub/core/query/attribute.py +55 -0
  63. infrahub/core/query/ipam.py +1 -0
  64. infrahub/core/query/node.py +23 -4
  65. infrahub/core/query/relationship.py +1 -0
  66. infrahub/core/registry.py +2 -2
  67. infrahub/core/relationship/constraints/count.py +1 -1
  68. infrahub/core/relationship/model.py +1 -1
  69. infrahub/core/schema/__init__.py +56 -0
  70. infrahub/core/schema/attribute_schema.py +4 -0
  71. infrahub/core/schema/basenode_schema.py +42 -2
  72. infrahub/core/schema/definitions/core/__init__.py +2 -0
  73. infrahub/core/schema/definitions/core/generator.py +2 -0
  74. infrahub/core/schema/definitions/core/group.py +16 -2
  75. infrahub/core/schema/definitions/internal.py +16 -3
  76. infrahub/core/schema/generated/attribute_schema.py +2 -2
  77. infrahub/core/schema/generated/base_node_schema.py +6 -1
  78. infrahub/core/schema/manager.py +22 -1
  79. infrahub/core/schema/node_schema.py +5 -2
  80. infrahub/core/schema/schema_branch.py +300 -8
  81. infrahub/core/schema/schema_branch_display.py +123 -0
  82. infrahub/core/schema/schema_branch_hfid.py +114 -0
  83. infrahub/core/validators/aggregated_checker.py +1 -1
  84. infrahub/core/validators/determiner.py +12 -1
  85. infrahub/core/validators/relationship/peer.py +1 -1
  86. infrahub/core/validators/tasks.py +1 -1
  87. infrahub/database/graph.py +21 -0
  88. infrahub/display_labels/__init__.py +0 -0
  89. infrahub/display_labels/gather.py +48 -0
  90. infrahub/display_labels/models.py +240 -0
  91. infrahub/display_labels/tasks.py +192 -0
  92. infrahub/display_labels/triggers.py +22 -0
  93. infrahub/events/branch_action.py +27 -1
  94. infrahub/events/group_action.py +1 -1
  95. infrahub/events/node_action.py +1 -1
  96. infrahub/generators/constants.py +7 -0
  97. infrahub/generators/models.py +7 -0
  98. infrahub/generators/tasks.py +34 -22
  99. infrahub/git/base.py +4 -1
  100. infrahub/git/integrator.py +23 -15
  101. infrahub/git/models.py +2 -1
  102. infrahub/git/repository.py +22 -5
  103. infrahub/git/tasks.py +66 -10
  104. infrahub/git/utils.py +123 -1
  105. infrahub/graphql/analyzer.py +1 -1
  106. infrahub/graphql/api/endpoints.py +14 -4
  107. infrahub/graphql/manager.py +4 -9
  108. infrahub/graphql/mutations/convert_object_type.py +11 -1
  109. infrahub/graphql/mutations/display_label.py +118 -0
  110. infrahub/graphql/mutations/generator.py +25 -7
  111. infrahub/graphql/mutations/hfid.py +125 -0
  112. infrahub/graphql/mutations/ipam.py +54 -35
  113. infrahub/graphql/mutations/main.py +27 -28
  114. infrahub/graphql/mutations/relationship.py +2 -2
  115. infrahub/graphql/mutations/resource_manager.py +2 -2
  116. infrahub/graphql/mutations/schema.py +5 -5
  117. infrahub/graphql/queries/resource_manager.py +1 -1
  118. infrahub/graphql/resolvers/resolver.py +2 -0
  119. infrahub/graphql/schema.py +4 -0
  120. infrahub/graphql/schema_sort.py +170 -0
  121. infrahub/graphql/types/branch.py +4 -1
  122. infrahub/graphql/types/enums.py +3 -0
  123. infrahub/groups/tasks.py +1 -1
  124. infrahub/hfid/__init__.py +0 -0
  125. infrahub/hfid/gather.py +48 -0
  126. infrahub/hfid/models.py +240 -0
  127. infrahub/hfid/tasks.py +191 -0
  128. infrahub/hfid/triggers.py +22 -0
  129. infrahub/lock.py +67 -16
  130. infrahub/message_bus/types.py +2 -1
  131. infrahub/middleware.py +26 -1
  132. infrahub/permissions/constants.py +2 -0
  133. infrahub/proposed_change/tasks.py +35 -17
  134. infrahub/server.py +21 -4
  135. infrahub/services/__init__.py +8 -5
  136. infrahub/services/adapters/http/__init__.py +5 -0
  137. infrahub/services/adapters/workflow/worker.py +14 -3
  138. infrahub/task_manager/event.py +5 -0
  139. infrahub/task_manager/models.py +7 -0
  140. infrahub/task_manager/task.py +73 -0
  141. infrahub/trigger/catalogue.py +4 -0
  142. infrahub/trigger/models.py +2 -0
  143. infrahub/trigger/setup.py +13 -4
  144. infrahub/trigger/tasks.py +6 -0
  145. infrahub/workers/dependencies.py +10 -1
  146. infrahub/workers/infrahub_async.py +10 -2
  147. infrahub/workflows/catalogue.py +80 -0
  148. infrahub/workflows/initialization.py +21 -0
  149. infrahub/workflows/utils.py +2 -1
  150. infrahub_sdk/checks.py +1 -1
  151. infrahub_sdk/client.py +13 -10
  152. infrahub_sdk/config.py +29 -2
  153. infrahub_sdk/ctl/cli_commands.py +2 -0
  154. infrahub_sdk/ctl/generator.py +4 -0
  155. infrahub_sdk/ctl/graphql.py +184 -0
  156. infrahub_sdk/ctl/schema.py +28 -9
  157. infrahub_sdk/generator.py +7 -1
  158. infrahub_sdk/graphql/__init__.py +12 -0
  159. infrahub_sdk/graphql/constants.py +1 -0
  160. infrahub_sdk/graphql/plugin.py +85 -0
  161. infrahub_sdk/graphql/query.py +77 -0
  162. infrahub_sdk/{graphql.py → graphql/renderers.py} +81 -73
  163. infrahub_sdk/graphql/utils.py +40 -0
  164. infrahub_sdk/protocols.py +14 -0
  165. infrahub_sdk/schema/__init__.py +70 -4
  166. infrahub_sdk/schema/repository.py +8 -0
  167. infrahub_sdk/spec/models.py +7 -0
  168. infrahub_sdk/spec/object.py +53 -44
  169. infrahub_sdk/spec/processors/__init__.py +0 -0
  170. infrahub_sdk/spec/processors/data_processor.py +10 -0
  171. infrahub_sdk/spec/processors/factory.py +34 -0
  172. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  173. infrahub_sdk/spec/range_expansion.py +1 -1
  174. infrahub_sdk/transforms.py +1 -1
  175. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/METADATA +7 -4
  176. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/RECORD +182 -143
  177. infrahub_testcontainers/container.py +115 -3
  178. infrahub_testcontainers/docker-compose-cluster.test.yml +6 -1
  179. infrahub_testcontainers/docker-compose.test.yml +6 -1
  180. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +0 -166
  181. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/LICENSE.txt +0 -0
  182. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/WHEEL +0 -0
  183. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import uuid
4
+ from enum import Enum
4
5
  from typing import Any, TypeAlias
5
6
 
6
7
  from infrahub_sdk.utils import deep_merge_dict
@@ -44,6 +45,21 @@ class SchemaExtension(HashableModel):
44
45
  nodes: list[NodeExtensionSchema] = Field(default_factory=list)
45
46
 
46
47
 
48
+ class SchemaWarningType(Enum):
49
+ DEPRECATION = "deprecation"
50
+
51
+
52
+ class SchemaWarningKind(BaseModel):
53
+ kind: str = Field(..., description="The kind impacted by the warning")
54
+ field: str | None = Field(default=None, description="The attribute or relationship impacted by the warning")
55
+
56
+
57
+ class SchemaWarning(BaseModel):
58
+ type: SchemaWarningType = Field(..., description="The type of warning")
59
+ kinds: list[SchemaWarningKind] = Field(default_factory=list, description="The kinds impacted by the warning")
60
+ message: str = Field(..., description="The message that describes the warning")
61
+
62
+
47
63
  class SchemaRoot(BaseModel):
48
64
  model_config = ConfigDict(extra="forbid")
49
65
 
@@ -80,6 +96,46 @@ class SchemaRoot(BaseModel):
80
96
 
81
97
  return errors
82
98
 
99
+ def gather_warnings(self) -> list[SchemaWarning]:
100
+ models = self.nodes + self.generics
101
+ warnings: list[SchemaWarning] = []
102
+ for model in models:
103
+ if model.display_labels is not None:
104
+ warnings.append(
105
+ SchemaWarning(
106
+ type=SchemaWarningType.DEPRECATION,
107
+ kinds=[SchemaWarningKind(kind=model.kind)],
108
+ message="display_labels are deprecated, use display_label instead",
109
+ )
110
+ )
111
+ if model.default_filter is not None:
112
+ warnings.append(
113
+ SchemaWarning(
114
+ type=SchemaWarningType.DEPRECATION,
115
+ kinds=[SchemaWarningKind(kind=model.kind)],
116
+ message="default_filter is deprecated",
117
+ )
118
+ )
119
+ for attribute in model.attributes:
120
+ if attribute.max_length is not None:
121
+ warnings.append(
122
+ SchemaWarning(
123
+ type=SchemaWarningType.DEPRECATION,
124
+ kinds=[SchemaWarningKind(kind=model.kind, field=attribute.name)],
125
+ message="Use of 'max_length' on attributes is deprecated, use parameters instead",
126
+ )
127
+ )
128
+ if attribute.min_length is not None:
129
+ warnings.append(
130
+ SchemaWarning(
131
+ type=SchemaWarningType.DEPRECATION,
132
+ kinds=[SchemaWarningKind(kind=model.kind, field=attribute.name)],
133
+ message="Use of 'min_length' on attributes is deprecated, use parameters instead",
134
+ )
135
+ )
136
+
137
+ return warnings
138
+
83
139
  def generate_uuid(self) -> None:
84
140
  """Generate UUID for all nodes, attributes & relationships
85
141
  Mainly useful during unit tests."""
@@ -68,6 +68,10 @@ class AttributeSchema(GeneratedAttributeSchema):
68
68
  def is_deprecated(self) -> bool:
69
69
  return bool(self.deprecation)
70
70
 
71
+ @property
72
+ def support_profiles(self) -> bool:
73
+ return self.read_only is False and self.optional is True
74
+
71
75
  def get_id(self) -> str:
72
76
  if self.id is None:
73
77
  raise InitializationError("The attribute schema has not been saved yet and doesn't have an id")
@@ -9,7 +9,7 @@ from enum import Enum
9
9
  from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, overload
10
10
 
11
11
  from infrahub_sdk.utils import compare_lists, intersection
12
- from pydantic import field_validator
12
+ from pydantic import ConfigDict, field_validator
13
13
 
14
14
  from infrahub.core.constants import HashableModelState, RelationshipCardinality, RelationshipKind
15
15
  from infrahub.core.models import HashableModel, HashableModelDiff
@@ -19,6 +19,7 @@ from .generated.base_node_schema import GeneratedBaseNodeSchema
19
19
  from .relationship_schema import RelationshipSchema
20
20
 
21
21
  if TYPE_CHECKING:
22
+ from pydantic.config import JsonDict
22
23
  from typing_extensions import Self
23
24
 
24
25
  from infrahub.core.schema import GenericSchema, NodeSchema
@@ -26,6 +27,7 @@ if TYPE_CHECKING:
26
27
 
27
28
 
28
29
  NODE_METADATA_ATTRIBUTES = ["_source", "_owner"]
30
+ NODE_PROPERTY_ATTRIBUTES = ["display_label", "human_friendly_id"]
29
31
  INHERITED = "INHERITED"
30
32
 
31
33
  OPTIONAL_TEXT_FIELDS = [
@@ -39,10 +41,43 @@ OPTIONAL_TEXT_FIELDS = [
39
41
  ]
40
42
 
41
43
 
44
+ def _json_schema_extra(schema: JsonDict) -> None:
45
+ """
46
+ Mutate the generated JSON Schema in place to:
47
+ - allow `null` for `display_labels`
48
+ - mark the non-null branch as deprecated
49
+ """
50
+ props = schema.get("properties")
51
+ if not isinstance(props, dict):
52
+ return
53
+ dl = props.get("display_labels")
54
+ if not isinstance(dl, dict):
55
+ return
56
+
57
+ if "anyOf" in dl:
58
+ dl["anyOf"] = [
59
+ {
60
+ "type": "array",
61
+ "items": {
62
+ "type": "string",
63
+ "deprecationMessage": "display_labels are deprecated use display_label instead",
64
+ },
65
+ },
66
+ {"type": "null"},
67
+ ]
68
+
69
+
42
70
  class BaseNodeSchema(GeneratedBaseNodeSchema):
43
71
  _exclude_from_hash: list[str] = ["attributes", "relationships"]
44
72
  _sort_by: list[str] = ["namespace", "name"]
45
73
 
74
+ model_config = ConfigDict(extra="forbid", json_schema_extra=_json_schema_extra)
75
+
76
+ @property
77
+ def is_schema_node(self) -> bool:
78
+ """Tell if this node represent a part of the schema. Not to confuse this with `is_node_schema`."""
79
+ return self.namespace == "Schema"
80
+
46
81
  @property
47
82
  def is_node_schema(self) -> bool:
48
83
  return False
@@ -240,6 +275,11 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
240
275
  return None
241
276
 
242
277
  def get_attribute(self, name: str) -> AttributeSchema:
278
+ if name == "human_friendly_id":
279
+ return AttributeSchema(name="human_friendly_id", kind="List", optional=True, branch=self.branch)
280
+ if name == "display_label":
281
+ return AttributeSchema(name="display_label", kind="Text", optional=True, branch=self.branch)
282
+
243
283
  for item in self.attributes:
244
284
  if item.name == name:
245
285
  return item
@@ -329,7 +369,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
329
369
 
330
370
  @property
331
371
  def valid_input_names(self) -> list[str]:
332
- return self.attribute_names + self.relationship_names + NODE_METADATA_ATTRIBUTES
372
+ return self.attribute_names + self.relationship_names + NODE_METADATA_ATTRIBUTES + NODE_PROPERTY_ATTRIBUTES
333
373
 
334
374
  @property
335
375
  def valid_local_names(self) -> list[str]:
@@ -29,6 +29,7 @@ from .core import core_node, core_task_target
29
29
  from .generator import core_generator_definition, core_generator_instance
30
30
  from .graphql_query import core_graphql_query
31
31
  from .group import (
32
+ core_generator_aware_group,
32
33
  core_generator_group,
33
34
  core_graphql_query_group,
34
35
  core_group,
@@ -128,6 +129,7 @@ core_models_mixed: dict[str, list] = {
128
129
  core_group_action,
129
130
  core_standard_group,
130
131
  core_generator_group,
132
+ core_generator_aware_group,
131
133
  core_graphql_query_group,
132
134
  core_repository_group,
133
135
  builtin_tag,
@@ -33,6 +33,8 @@ core_generator_definition = NodeSchema(
33
33
  Attr(name="file_path", kind="Text"),
34
34
  Attr(name="class_name", kind="Text"),
35
35
  Attr(name="convert_query_response", kind="Boolean", optional=True, default_value=False),
36
+ Attr(name="execute_in_proposed_change", kind="Boolean", optional=True, default_value=True),
37
+ Attr(name="execute_after_merge", kind="Boolean", optional=True, default_value=True),
36
38
  ],
37
39
  relationships=[
38
40
  Rel(
@@ -70,10 +70,10 @@ core_standard_group = NodeSchema(
70
70
  core_generator_group = NodeSchema(
71
71
  name="GeneratorGroup",
72
72
  namespace="Core",
73
- description="Group of nodes that are created by a generator.",
73
+ description="Group of nodes that are created by a generator. (local)",
74
74
  include_in_menu=False,
75
75
  icon="mdi:state-machine",
76
- label="Generator Group",
76
+ label="Generator Group (local)",
77
77
  default_filter="name__value",
78
78
  order_by=["name__value"],
79
79
  display_labels=["name__value"],
@@ -82,6 +82,20 @@ core_generator_group = NodeSchema(
82
82
  generate_profile=False,
83
83
  )
84
84
 
85
+ core_generator_aware_group = NodeSchema(
86
+ name="GeneratorAwareGroup",
87
+ namespace="Core",
88
+ description="Group of nodes that are created by a generator. (Aware)",
89
+ include_in_menu=False,
90
+ icon="mdi:state-machine",
91
+ label="Generator Group (aware)",
92
+ default_filter="name__value",
93
+ order_by=["name__value"],
94
+ display_labels=["name__value"],
95
+ branch=BranchSupportType.AWARE,
96
+ inherit_from=[InfrahubKind.GENERICGROUP],
97
+ generate_profile=False,
98
+ )
85
99
 
86
100
  core_graphql_query_group = NodeSchema(
87
101
  name="GraphQLQueryGroup",
@@ -179,7 +179,9 @@ class SchemaNode(BaseModel):
179
179
  default_filter: str | None = None
180
180
  attributes: list[SchemaAttribute]
181
181
  relationships: list[SchemaRelationship]
182
+ display_label: str | None = None
182
183
  display_labels: list[str]
184
+ uniqueness_constraints: list[list[str]] | None = None
183
185
 
184
186
  def to_dict(self) -> dict[str, Any]:
185
187
  return {
@@ -194,7 +196,9 @@ class SchemaNode(BaseModel):
194
196
  if attribute.name not in ["id", "attributes", "relationships"]
195
197
  ],
196
198
  "relationships": [relationship.to_dict() for relationship in self.relationships],
199
+ "display_label": self.display_label,
197
200
  "display_labels": self.display_labels,
201
+ "uniqueness_constraints": self.uniqueness_constraints,
198
202
  }
199
203
 
200
204
  def without_duplicates(self, other: SchemaNode) -> SchemaNode:
@@ -292,11 +296,18 @@ base_node_schema = SchemaNode(
292
296
  optional=True,
293
297
  extra={"update": UpdateSupport.ALLOWED},
294
298
  ),
299
+ SchemaAttribute(
300
+ name="display_label",
301
+ kind="Text",
302
+ description="Attribute or Jinja2 template to use to generate the display label",
303
+ optional=True,
304
+ extra={"update": UpdateSupport.ALLOWED},
305
+ ),
295
306
  SchemaAttribute(
296
307
  name="display_labels",
297
308
  kind="List",
298
309
  internal_kind=str,
299
- description="List of attributes to use to generate the display label",
310
+ description="List of attributes to use to generate the display label (deprecated)",
300
311
  optional=True,
301
312
  extra={"update": UpdateSupport.ALLOWED},
302
313
  ),
@@ -465,6 +476,7 @@ attribute_schema = SchemaNode(
465
476
  include_in_menu=False,
466
477
  default_filter=None,
467
478
  display_labels=["name__value"],
479
+ uniqueness_constraints=[["name__value", "node"]],
468
480
  attributes=[
469
481
  SchemaAttribute(
470
482
  name="id",
@@ -556,7 +568,7 @@ attribute_schema = SchemaNode(
556
568
  "Mainly relevant for internal object.",
557
569
  default_value=False,
558
570
  optional=True,
559
- extra={"update": UpdateSupport.ALLOWED},
571
+ extra={"update": UpdateSupport.MIGRATION_REQUIRED},
560
572
  ),
561
573
  SchemaAttribute(
562
574
  name="unique",
@@ -573,7 +585,7 @@ attribute_schema = SchemaNode(
573
585
  default_value=False,
574
586
  override_default_value=False,
575
587
  optional=True,
576
- extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
588
+ extra={"update": UpdateSupport.MIGRATION_REQUIRED},
577
589
  ),
578
590
  SchemaAttribute(
579
591
  name="branch",
@@ -669,6 +681,7 @@ relationship_schema = SchemaNode(
669
681
  include_in_menu=False,
670
682
  default_filter=None,
671
683
  display_labels=["name__value"],
684
+ uniqueness_constraints=[["name__value", "node"]],
672
685
  attributes=[
673
686
  SchemaAttribute(
674
687
  name="id",
@@ -78,7 +78,7 @@ class GeneratedAttributeSchema(HashableModel):
78
78
  read_only: bool = Field(
79
79
  default=False,
80
80
  description="Set the attribute as Read-Only, users won't be able to change its value. Mainly relevant for internal object.",
81
- json_schema_extra={"update": "allowed"},
81
+ json_schema_extra={"update": "migration_required"},
82
82
  )
83
83
  unique: bool = Field(
84
84
  default=False,
@@ -88,7 +88,7 @@ class GeneratedAttributeSchema(HashableModel):
88
88
  optional: bool = Field(
89
89
  default=False,
90
90
  description="Indicate if this attribute is mandatory or optional.",
91
- json_schema_extra={"update": "validate_constraint"},
91
+ json_schema_extra={"update": "migration_required"},
92
92
  )
93
93
  branch: BranchSupportType | None = Field(
94
94
  default=None,
@@ -58,9 +58,14 @@ class GeneratedBaseNodeSchema(HashableModel):
58
58
  description="Human friendly and unique identifier for the object.",
59
59
  json_schema_extra={"update": "allowed"},
60
60
  )
61
+ display_label: str | None = Field(
62
+ default=None,
63
+ description="Attribute or Jinja2 template to use to generate the display label",
64
+ json_schema_extra={"update": "allowed"},
65
+ )
61
66
  display_labels: list[str] | None = Field(
62
67
  default=None,
63
- description="List of attributes to use to generate the display label",
68
+ description="List of attributes to use to generate the display label (deprecated)",
64
69
  json_schema_extra={"update": "allowed"},
65
70
  )
66
71
  include_in_menu: bool | None = Field(
@@ -2,6 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
+ from cachetools import LRUCache
6
+ from infrahub_sdk.schema import BranchSchema as SDKBranchSchema
7
+
5
8
  from infrahub import lock
6
9
  from infrahub.core.manager import NodeManager
7
10
  from infrahub.core.models import (
@@ -40,6 +43,8 @@ class SchemaManager(NodeManager):
40
43
  def __init__(self) -> None:
41
44
  self._cache: dict[int, Any] = {}
42
45
  self._branches: dict[str, SchemaBranch] = {}
46
+ self._branch_hash_by_name: dict[str, str] = {}
47
+ self._sdk_branches: LRUCache[str, SDKBranchSchema] = LRUCache(maxsize=10)
43
48
 
44
49
  def _get_from_cache(self, key: int) -> Any:
45
50
  return self._cache[key]
@@ -140,12 +145,26 @@ class SchemaManager(NodeManager):
140
145
  if name in self._branches:
141
146
  return self._branches[name]
142
147
 
143
- self._branches[name] = SchemaBranch(cache=self._cache, name=name)
148
+ self.set_schema_branch(name, schema=SchemaBranch(cache=self._cache, name=name))
144
149
  return self._branches[name]
145
150
 
151
+ def get_sdk_schema_branch(self, name: str) -> SDKBranchSchema:
152
+ schema_hash = self._branch_hash_by_name[name]
153
+ branch_schema = self._sdk_branches.get(schema_hash)
154
+ if not branch_schema:
155
+ self._sdk_branches[schema_hash] = SDKBranchSchema.from_api_response(
156
+ data=self._branches[name].to_dict_api_schema_object()
157
+ )
158
+
159
+ return self._sdk_branches[schema_hash]
160
+
146
161
  def set_schema_branch(self, name: str, schema: SchemaBranch) -> None:
147
162
  schema.name = name
148
163
  self._branches[name] = schema
164
+ self._branch_hash_by_name[name] = schema.get_hash()
165
+
166
+ def has_schema_branch(self, name: str) -> bool:
167
+ return name in self._branches
149
168
 
150
169
  def process_schema_branch(self, name: str) -> None:
151
170
  schema_branch = self.get_schema_branch(name=name)
@@ -764,6 +783,8 @@ class SchemaManager(NodeManager):
764
783
  for branch_name in list(self._branches.keys()):
765
784
  if branch_name not in active_branches:
766
785
  del self._branches[branch_name]
786
+ if branch_name in self._branch_hash_by_name:
787
+ del self._branch_hash_by_name[branch_name]
767
788
  removed_branches.append(branch_name)
768
789
 
769
790
  for hash_key in list(self._cache.keys()):
@@ -90,6 +90,7 @@ class NodeSchema(GeneratedNodeSchema):
90
90
 
91
91
  properties_to_inherit = [
92
92
  "human_friendly_id",
93
+ "display_label",
93
94
  "display_labels",
94
95
  "default_filter",
95
96
  "menu_placement",
@@ -129,10 +130,12 @@ class NodeSchema(GeneratedNodeSchema):
129
130
  item_idx = existing_inherited_relationships[relationship.name]
130
131
  self.relationships[item_idx].update_from_generic(other=new_relationship)
131
132
 
132
- def get_hierarchy_schema(self, db: InfrahubDatabase, branch: Branch | str | None = None) -> GenericSchema:
133
+ def get_hierarchy_schema(
134
+ self, db: InfrahubDatabase, branch: Branch | str | None = None, duplicate: bool = False
135
+ ) -> GenericSchema:
133
136
  if not self.hierarchy:
134
137
  raise ValueError("The node is not part of a hierarchy")
135
- schema = db.schema.get(name=self.hierarchy, branch=branch)
138
+ schema = db.schema.get(name=self.hierarchy, branch=branch, duplicate=duplicate)
136
139
  if not isinstance(schema, GenericSchema):
137
140
  raise TypeError
138
141
  return schema