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
@@ -66,6 +66,8 @@ from ... import config
66
66
  from ..constants.schema import PARENT_CHILD_IDENTIFIER
67
67
  from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
68
68
  from .schema_branch_computed import ComputedAttributes
69
+ from .schema_branch_display import DisplayLabels
70
+ from .schema_branch_hfid import HFIDs
69
71
 
70
72
  log = get_logger()
71
73
 
@@ -77,6 +79,8 @@ class SchemaBranch:
77
79
  name: str | None = None,
78
80
  data: dict[str, dict[str, str]] | None = None,
79
81
  computed_attributes: ComputedAttributes | None = None,
82
+ display_labels: DisplayLabels | None = None,
83
+ hfids: HFIDs | None = None,
80
84
  ):
81
85
  self._cache: dict[str, NodeSchema | GenericSchema] = cache
82
86
  self.name: str | None = name
@@ -85,6 +89,8 @@ class SchemaBranch:
85
89
  self.profiles: dict[str, str] = {}
86
90
  self.templates: dict[str, str] = {}
87
91
  self.computed_attributes = computed_attributes or ComputedAttributes()
92
+ self.display_labels = display_labels or DisplayLabels()
93
+ self.hfids = hfids or HFIDs()
88
94
 
89
95
  if data:
90
96
  self.nodes = data.get("nodes", {})
@@ -163,6 +169,14 @@ class SchemaBranch:
163
169
  "templates": {name: self.get(name, duplicate=duplicate) for name in self.templates},
164
170
  }
165
171
 
172
+ def to_dict_api_schema_object(self) -> dict[str, list[dict]]:
173
+ return {
174
+ "nodes": [self.get(name, duplicate=False).model_dump() for name in self.nodes],
175
+ "profiles": [self.get(name, duplicate=False).model_dump() for name in self.profiles],
176
+ "generics": [self.get(name, duplicate=False).model_dump() for name in self.generics],
177
+ "templates": [self.get(name, duplicate=False).model_dump() for name in self.templates],
178
+ }
179
+
166
180
  @classmethod
167
181
  def from_dict_schema_object(cls, data: dict) -> Self:
168
182
  type_mapping = {
@@ -270,6 +284,8 @@ class SchemaBranch:
270
284
  data=copy.deepcopy(self.to_dict()),
271
285
  cache=self._cache,
272
286
  computed_attributes=self.computed_attributes.duplicate(),
287
+ display_labels=self.display_labels.duplicate(),
288
+ hfids=self.hfids.duplicate(),
273
289
  )
274
290
 
275
291
  def set(self, name: str, schema: MainSchemaTypes) -> str:
@@ -312,14 +328,23 @@ class SchemaBranch:
312
328
  elif name in self.templates:
313
329
  key = self.templates[name]
314
330
 
315
- if key and duplicate:
316
- return self._cache[key].duplicate()
317
- if key and not duplicate:
318
- return self._cache[key]
331
+ if not key:
332
+ raise SchemaNotFoundError(
333
+ branch_name=self.name, identifier=name, message=f"Unable to find the schema {name!r} in the registry"
334
+ )
319
335
 
320
- raise SchemaNotFoundError(
321
- branch_name=self.name, identifier=name, message=f"Unable to find the schema {name!r} in the registry"
322
- )
336
+ schema: MainSchemaTypes | None = None
337
+ try:
338
+ schema = self._cache[key]
339
+ except KeyError:
340
+ pass
341
+
342
+ if not schema:
343
+ raise ValueError(f"Schema {name!r} on branch {self.name} has incorrect hash: {key!r}")
344
+
345
+ if duplicate:
346
+ return schema.duplicate()
347
+ return schema
323
348
 
324
349
  def get_node(self, name: str, duplicate: bool = True) -> NodeSchema:
325
350
  """Access a specific NodeSchema, defined by its kind."""
@@ -504,6 +529,9 @@ class SchemaBranch:
504
529
  self.process_post_validation()
505
530
 
506
531
  def process_pre_validation(self) -> None:
532
+ self.process_nodes_state()
533
+ self.process_attributes_state()
534
+ self.process_relationships_state()
507
535
  self.generate_identifiers()
508
536
  self.process_default_values()
509
537
  self.process_deprecations()
@@ -530,11 +558,13 @@ class SchemaBranch:
530
558
  self.sync_uniqueness_constraints_and_unique_attributes()
531
559
  self.validate_uniqueness_constraints()
532
560
  self.validate_display_labels()
561
+ self.validate_display_label()
533
562
  self.validate_order_by()
534
563
  self.validate_default_filters()
535
564
  self.validate_parent_component()
536
565
  self.validate_human_friendly_id()
537
566
  self.validate_required_relationships()
567
+ self.validate_inherited_relationships_fields()
538
568
 
539
569
  def process_post_validation(self) -> None:
540
570
  self.cleanup_inherited_elements()
@@ -544,6 +574,7 @@ class SchemaBranch:
544
574
  self.process_dropdowns()
545
575
  self.process_relationships()
546
576
  self.process_human_friendly_id()
577
+ self.register_human_friendly_id()
547
578
 
548
579
  def _generate_identifier_string(self, node_kind: str, peer_kind: str) -> str:
549
580
  return "__".join(sorted([node_kind, peer_kind])).lower()
@@ -752,6 +783,36 @@ class SchemaBranch:
752
783
  element_name=element_name,
753
784
  )
754
785
 
786
+ def validate_display_label(self) -> None:
787
+ self.display_labels = DisplayLabels()
788
+ for name in self.all_names:
789
+ node_schema = self.get(name=name, duplicate=False)
790
+
791
+ if node_schema.display_label is None and node_schema.display_labels:
792
+ update_candidate = self.get(name=name, duplicate=True)
793
+ if len(node_schema.display_labels) == 1:
794
+ # If the previous display_labels consist of a single attribute convert
795
+ # it to an attribute based display label
796
+ update_candidate.display_label = _format_display_label_component(
797
+ component=node_schema.display_labels[0]
798
+ )
799
+ else:
800
+ # If the previous display label consists of multiple attributes
801
+ # convert it to a Jinja2 based display label
802
+ update_candidate.display_label = " ".join(
803
+ [
804
+ f"{{{{ {_format_display_label_component(component=display_label)} }}}}"
805
+ for display_label in node_schema.display_labels
806
+ ]
807
+ )
808
+ self.set(name=name, schema=update_candidate)
809
+
810
+ node_schema = self.get(name=name, duplicate=False)
811
+ if not node_schema.display_label:
812
+ continue
813
+
814
+ self._validate_display_label(node=node_schema)
815
+
755
816
  def validate_display_labels(self) -> None:
756
817
  for name in self.all_names:
757
818
  node_schema = self.get(name=name, duplicate=False)
@@ -860,7 +921,14 @@ class SchemaBranch:
860
921
  # Mapping relationship identifiers -> list of attributes paths
861
922
  rel_schemas_to_paths: dict[str, tuple[MainSchemaTypes, list[str]]] = {}
862
923
 
924
+ visited_paths: list[str] = []
863
925
  for hfid_path in node_schema.human_friendly_id:
926
+ if config.SETTINGS.main.schema_strict_mode and hfid_path in visited_paths:
927
+ raise ValidationError(
928
+ f"HFID of {node_schema.kind} cannot use the same path more than once: {hfid_path}"
929
+ )
930
+
931
+ visited_paths.append(hfid_path)
864
932
  schema_path = self.validate_schema_path(
865
933
  node_schema=node_schema,
866
934
  path=hfid_path,
@@ -1137,6 +1205,50 @@ class SchemaBranch:
1137
1205
  node=node_schema, attribute=attribute, generic=generic_schema
1138
1206
  )
1139
1207
 
1208
+ def _validate_display_label(self, node: MainSchemaTypes) -> None:
1209
+ if not node.display_label:
1210
+ return
1211
+
1212
+ if not any(c in node.display_label for c in "{}"):
1213
+ schema_path = self.validate_schema_path(
1214
+ node_schema=node,
1215
+ path=node.display_label,
1216
+ allowed_path_types=SchemaElementPathType.ATTR_WITH_PROP,
1217
+ element_name="display_label - non Jinja2",
1218
+ )
1219
+ if schema_path.attribute_schema and node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
1220
+ self.display_labels.register_attribute_based_display_label(
1221
+ kind=node.kind, attribute_name=schema_path.attribute_schema.name
1222
+ )
1223
+ return
1224
+
1225
+ jinja_template = Jinja2Template(template=node.display_label)
1226
+ try:
1227
+ variables = jinja_template.get_variables()
1228
+ jinja_template.validate(restricted=config.SETTINGS.security.restrict_untrusted_jinja2_filters)
1229
+ except (JinjaTemplateOperationViolationError, JinjaTemplateError) as exc:
1230
+ raise ValueError(
1231
+ f"{node.kind}: display_label is set to a jinja2 template, but has an invalid template: {exc.message}"
1232
+ ) from exc
1233
+
1234
+ allowed_path_types = (
1235
+ SchemaElementPathType.ATTR_WITH_PROP
1236
+ | SchemaElementPathType.REL_ONE_MANDATORY_ATTR_WITH_PROP
1237
+ | SchemaElementPathType.REL_ONE_ATTR_WITH_PROP
1238
+ )
1239
+ for variable in variables:
1240
+ schema_path = self.validate_schema_path(
1241
+ node_schema=node, path=variable, allowed_path_types=allowed_path_types, element_name="display_label"
1242
+ )
1243
+
1244
+ if schema_path.is_type_attribute and schema_path.active_attribute_schema.name == "display_label":
1245
+ raise ValueError(f"{node.kind}: display_label the '{variable}' variable is a reference to itself")
1246
+
1247
+ if node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
1248
+ self.display_labels.register_template_schema_path(
1249
+ kind=node.kind, schema_path=schema_path, template=node.display_label
1250
+ )
1251
+
1140
1252
  def _validate_computed_attribute(self, node: NodeSchema, attribute: AttributeSchema) -> None:
1141
1253
  if not attribute.computed_attribute or attribute.computed_attribute.kind == ComputedAttributeKind.USER:
1142
1254
  return
@@ -1229,6 +1341,81 @@ class SchemaBranch:
1229
1341
  f"{node.kind}: Relationship {rel.name!r} max_count must be 0 or greater than 1 when cardinality is MANY"
1230
1342
  )
1231
1343
 
1344
+ def validate_inherited_relationships_fields(self) -> None:
1345
+ for name in self.node_names:
1346
+ node_schema = self.get(name=name, duplicate=False)
1347
+ if not node_schema.inherit_from:
1348
+ continue
1349
+
1350
+ self.validate_node_inherited_relationship_fields(node_schema)
1351
+
1352
+ def validate_node_inherited_relationship_fields(self, node_schema: NodeSchema) -> None:
1353
+ generics = [self.get(name=node_name, duplicate=False) for node_name in node_schema.inherit_from]
1354
+ relationship_names = [node.relationship_names for node in generics]
1355
+ related_relationship_names = set().union(
1356
+ *[
1357
+ set(relationship_name_a) & set(relationship_name_b)
1358
+ for index, relationship_name_a in enumerate(relationship_names)
1359
+ for relationship_name_b in relationship_names[index + 1 :]
1360
+ ]
1361
+ )
1362
+ # Check that the relationship properties match
1363
+ # for every generic node in generics list having related relationship names
1364
+ for index, generic_a in enumerate(generics):
1365
+ for generic_b in generics[index + 1 :]:
1366
+ for relationship_name in related_relationship_names:
1367
+ try:
1368
+ relationship_a = generic_a.get_relationship(name=relationship_name)
1369
+ relationship_b = generic_b.get_relationship(name=relationship_name)
1370
+ except ValueError:
1371
+ continue
1372
+
1373
+ matched, _property = self._check_relationship_properties_match(
1374
+ relationship_a=relationship_a, relationship_b=relationship_b
1375
+ )
1376
+ if not matched:
1377
+ raise ValueError(
1378
+ f"{node_schema.kind} inherits from '{generic_a.kind}' & '{generic_b.kind}'"
1379
+ f" with different '{_property}' on the '{relationship_name}' relationship"
1380
+ )
1381
+
1382
+ def _check_relationship_properties_match(
1383
+ self, relationship_a: RelationshipSchema, relationship_b: RelationshipSchema
1384
+ ) -> tuple[bool, str | None]:
1385
+ compulsorily_matching_properties = (
1386
+ "name",
1387
+ "peer",
1388
+ "kind",
1389
+ "identifier",
1390
+ "cardinality",
1391
+ "min_count",
1392
+ "max_count",
1393
+ "common_parent",
1394
+ "common_relatives",
1395
+ "optional",
1396
+ "branch",
1397
+ "direction",
1398
+ "on_delete",
1399
+ "read_only",
1400
+ "hierarchical",
1401
+ "allow_override",
1402
+ )
1403
+ for _property in compulsorily_matching_properties:
1404
+ if not hasattr(relationship_a, _property) or not hasattr(relationship_b, _property):
1405
+ continue
1406
+
1407
+ equal_delete_actions = (None, RelationshipDeleteBehavior.NO_ACTION)
1408
+ if (
1409
+ _property == "on_delete"
1410
+ and getattr(relationship_a, _property) in equal_delete_actions
1411
+ and getattr(relationship_b, _property) in equal_delete_actions
1412
+ ):
1413
+ continue
1414
+
1415
+ if getattr(relationship_a, _property) != getattr(relationship_b, _property):
1416
+ return False, _property
1417
+ return True, None
1418
+
1232
1419
  def process_dropdowns(self) -> None:
1233
1420
  for name in self.all_names:
1234
1421
  node = self.get(name=name, duplicate=False)
@@ -1357,6 +1544,34 @@ class SchemaBranch:
1357
1544
  node.uniqueness_constraints = [hfid_uniqueness_constraint]
1358
1545
  self.set(name=node.kind, schema=node)
1359
1546
 
1547
+ def register_human_friendly_id(self) -> None:
1548
+ """Register HFID automations
1549
+
1550
+ Register the HFIDs after all processing and validation has been done.
1551
+ """
1552
+
1553
+ self.hfids = HFIDs()
1554
+ for name in self.generic_names_without_templates + self.node_names:
1555
+ node_schema = self.get(name=name, duplicate=False)
1556
+
1557
+ if not node_schema.human_friendly_id:
1558
+ continue
1559
+
1560
+ allowed_types = SchemaElementPathType.ATTR_WITH_PROP | SchemaElementPathType.REL_ONE_MANDATORY_ATTR
1561
+
1562
+ for hfid_path in node_schema.human_friendly_id:
1563
+ schema_path = self.validate_schema_path(
1564
+ node_schema=node_schema,
1565
+ path=hfid_path,
1566
+ allowed_path_types=allowed_types,
1567
+ element_name="human_friendly_id",
1568
+ )
1569
+
1570
+ if node_schema.is_node_schema and node_schema.namespace not in ["Schema", "Internal"]:
1571
+ self.hfids.register_hfid_schema_path(
1572
+ kind=node_schema.kind, schema_path=schema_path, hfid=node_schema.human_friendly_id
1573
+ )
1574
+
1360
1575
  def process_hierarchy(self) -> None:
1361
1576
  for name in self.nodes.keys():
1362
1577
  node = self.get_node(name=name, duplicate=False)
@@ -1612,10 +1827,48 @@ class SchemaBranch:
1612
1827
 
1613
1828
  self.set(name=name, schema=node)
1614
1829
 
1830
+ def process_relationships_state(self) -> None:
1831
+ for name in self.node_names + self.generic_names_without_templates:
1832
+ node = self.get(name=name, duplicate=False)
1833
+ if node.id or (not node.id and not node.relationships):
1834
+ continue
1835
+
1836
+ filtered_relationships = [
1837
+ relationship for relationship in node.relationships if relationship.state != HashableModelState.ABSENT
1838
+ ]
1839
+ if len(filtered_relationships) == len(node.relationships):
1840
+ continue
1841
+ updated_node = node.duplicate()
1842
+ updated_node.relationships = filtered_relationships
1843
+ self.set(name=name, schema=updated_node)
1844
+
1845
+ def process_attributes_state(self) -> None:
1846
+ for name in self.node_names + self.generic_names_without_templates:
1847
+ node = self.get(name=name, duplicate=False)
1848
+ if not node.attributes:
1849
+ continue
1850
+
1851
+ filtered_attributes = [
1852
+ attribute for attribute in node.attributes if attribute.state != HashableModelState.ABSENT
1853
+ ]
1854
+ if len(filtered_attributes) == len(node.attributes):
1855
+ continue
1856
+ updated_node = node.duplicate()
1857
+ updated_node.attributes = filtered_attributes
1858
+ self.set(name=name, schema=updated_node)
1859
+
1860
+ def process_nodes_state(self) -> None:
1861
+ for name in self.node_names + self.generic_names_without_templates:
1862
+ node = self.get(name=name, duplicate=False)
1863
+ if not node.id and node.state == HashableModelState.ABSENT:
1864
+ self.delete(name=name)
1865
+
1615
1866
  def _generate_weight_generics(self) -> None:
1616
1867
  """Generate order_weight for all generic schemas."""
1617
1868
  for name in self.generic_names:
1618
1869
  node = self.get(name=name, duplicate=False)
1870
+ if node.namespace == "Template":
1871
+ continue
1619
1872
 
1620
1873
  items_to_update = [item for item in node.attributes + node.relationships if not item.order_weight]
1621
1874
 
@@ -1679,10 +1932,36 @@ class SchemaBranch:
1679
1932
 
1680
1933
  self.set(name=name, schema=template)
1681
1934
 
1935
+ def _generate_generics_templates_weight(self) -> None:
1936
+ """Generate order_weight for generic templates.
1937
+
1938
+ The order of the fields for the template must respect the order of the node.
1939
+ """
1940
+ for name in self.generic_names:
1941
+ generic_node = self.get(name=name, duplicate=False)
1942
+ try:
1943
+ template = self.get(name=self._get_object_template_kind(node_kind=generic_node.kind), duplicate=True)
1944
+ except SchemaNotFoundError:
1945
+ continue
1946
+
1947
+ generic_node_weights = {
1948
+ item.name: item.order_weight
1949
+ for item in generic_node.attributes + generic_node.relationships
1950
+ if item.order_weight is not None
1951
+ }
1952
+
1953
+ for item in template.attributes + template.relationships:
1954
+ if item.order_weight:
1955
+ continue
1956
+ item.order_weight = generic_node_weights[item.name] if item.name in generic_node_weights else None
1957
+
1958
+ self.set(name=template.kind, schema=template)
1959
+
1682
1960
  def generate_weight(self) -> None:
1683
1961
  self._generate_weight_generics()
1684
1962
  self._generate_weight_nodes_profiles()
1685
1963
  self._generate_weight_templates()
1964
+ self._generate_generics_templates_weight()
1686
1965
 
1687
1966
  def cleanup_inherited_elements(self) -> None:
1688
1967
  for name in self.node_names:
@@ -2008,7 +2287,7 @@ class SchemaBranch:
2008
2287
  )
2009
2288
 
2010
2289
  for node_attr in node.attributes:
2011
- if node_attr.read_only or node_attr.optional is False:
2290
+ if not node_attr.support_profiles:
2012
2291
  continue
2013
2292
  attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
2014
2293
  attr = attr_schema_class(
@@ -2313,3 +2592,16 @@ class SchemaBranch:
2313
2592
  updated_used_by_node = set(chain(template_schema_kinds, set(core_node_schema.used_by)))
2314
2593
  core_node_schema.used_by = sorted(updated_used_by_node)
2315
2594
  self.set(name=InfrahubKind.NODE, schema=core_node_schema)
2595
+
2596
+
2597
+ def _format_display_label_component(component: str) -> str:
2598
+ """Return correct format for display_label.
2599
+
2600
+ Previously both the format of 'name' and 'name__value' was
2601
+ supported this function ensures that the proper 'name__value'
2602
+ format is used
2603
+ """
2604
+ if "__" in component:
2605
+ return component
2606
+
2607
+ return f"{component}__value"
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from infrahub.core.schema import SchemaAttributePath
10
+
11
+
12
+ @dataclass
13
+ class TemplateLabel:
14
+ template: str
15
+ attributes: set[str] = field(default_factory=set)
16
+ relationships: set[str] = field(default_factory=set)
17
+ filter_key: str = "ids"
18
+
19
+ @property
20
+ def fields(self) -> list[str]:
21
+ return sorted(list(self.attributes) + list(self.relationships))
22
+
23
+ @property
24
+ def has_related_components(self) -> bool:
25
+ """Indicate if the associated template use variables from relationships"""
26
+ return len(self.relationships) > 0
27
+
28
+ def get_hash(self) -> str:
29
+ return hashlib.md5(self.template.encode(), usedforsecurity=False).hexdigest()
30
+
31
+
32
+ @dataclass
33
+ class RelationshipIdentifier:
34
+ kind: str
35
+ filter_key: str
36
+ template: str
37
+
38
+ def __hash__(self) -> int:
39
+ return hash(f"{self.kind}::{self.filter_key}::{self.template}")
40
+
41
+
42
+ @dataclass
43
+ class RelationshipTriggers:
44
+ attributes: dict[str, set[RelationshipIdentifier]] = field(default_factory=dict)
45
+
46
+
47
+ class DisplayLabels:
48
+ def __init__(
49
+ self,
50
+ template_based_display_labels: dict[str, TemplateLabel] | None = None,
51
+ template_relationship_triggers: dict[str, RelationshipTriggers] | None = None,
52
+ ) -> None:
53
+ self._template_based_display_labels: dict[str, TemplateLabel] = template_based_display_labels or {}
54
+ self._template_relationship_triggers: dict[str, RelationshipTriggers] = template_relationship_triggers or {}
55
+
56
+ def duplicate(self) -> DisplayLabels:
57
+ """Clone the current object."""
58
+ return self.__class__(
59
+ template_based_display_labels=deepcopy(self._template_based_display_labels),
60
+ template_relationship_triggers=deepcopy(self._template_relationship_triggers),
61
+ )
62
+
63
+ def register_attribute_based_display_label(self, kind: str, attribute_name: str) -> None:
64
+ """Register nodes where the display label consists of a single defined attribute name."""
65
+ self._template_based_display_labels[kind] = TemplateLabel(
66
+ template=f"{{{{ {attribute_name}__value }}}}", attributes={attribute_name}
67
+ )
68
+
69
+ def register_template_schema_path(self, kind: str, schema_path: SchemaAttributePath, template: str) -> None:
70
+ """Register Jinja2 template based display labels using the schema path of each impacted variable in the node."""
71
+
72
+ if kind not in self._template_based_display_labels:
73
+ self._template_based_display_labels[kind] = TemplateLabel(template=template)
74
+
75
+ if schema_path.is_type_attribute:
76
+ self._template_based_display_labels[kind].attributes.add(schema_path.active_attribute_schema.name)
77
+ elif schema_path.is_type_relationship and schema_path.related_schema:
78
+ self._template_based_display_labels[kind].relationships.add(schema_path.active_relationship_schema.name)
79
+ if schema_path.related_schema.kind not in self._template_relationship_triggers:
80
+ self._template_relationship_triggers[schema_path.related_schema.kind] = RelationshipTriggers()
81
+ if (
82
+ schema_path.active_attribute_schema.name
83
+ not in self._template_relationship_triggers[schema_path.related_schema.kind].attributes
84
+ ):
85
+ self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
86
+ schema_path.active_attribute_schema.name
87
+ ] = set()
88
+ self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
89
+ schema_path.active_attribute_schema.name
90
+ ].add(
91
+ RelationshipIdentifier(
92
+ kind=kind, filter_key=f"{schema_path.active_relationship_schema.name}__ids", template=template
93
+ )
94
+ )
95
+
96
+ def targets_node(self, kind: str) -> bool:
97
+ """Indicates if there is a display_label defined for the targeted node"""
98
+ return kind in self._template_based_display_labels
99
+
100
+ def get_template_node(self, kind: str) -> TemplateLabel:
101
+ """Return node kinds together with their template definitions."""
102
+ return self._template_based_display_labels[kind]
103
+
104
+ def get_template_nodes(self) -> dict[str, TemplateLabel]:
105
+ """Return node kinds together with their template definitions."""
106
+ return self._template_based_display_labels
107
+
108
+ def get_related_trigger_nodes(self) -> dict[str, RelationshipTriggers]:
109
+ """Return node kinds that other nodes use within their templates for display_labels."""
110
+ return self._template_relationship_triggers
111
+
112
+ def get_related_template(self, related_kind: str, target_kind: str) -> TemplateLabel:
113
+ relationship_trigger = self._template_relationship_triggers[related_kind]
114
+ for applicable_kinds in relationship_trigger.attributes.values():
115
+ for relationship_identifier in applicable_kinds:
116
+ if target_kind == relationship_identifier.kind:
117
+ template_label = self.get_template_node(kind=target_kind)
118
+ template_label.filter_key = relationship_identifier.filter_key
119
+ return template_label
120
+
121
+ raise ValueError(
122
+ f"Unable to find registered template for {target_kind} registered on related node {related_kind}"
123
+ )
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from infrahub.core.schema import SchemaAttributePath
10
+
11
+
12
+ @dataclass
13
+ class HFIDDefinition:
14
+ hfid: list[str]
15
+ attributes: set[str] = field(default_factory=set)
16
+ relationships: set[str] = field(default_factory=set)
17
+ filter_key: str = "ids"
18
+
19
+ @property
20
+ def fields(self) -> list[str]:
21
+ return sorted(list(self.attributes) + list(self.relationships))
22
+
23
+ @property
24
+ def has_related_components(self) -> bool:
25
+ """Indicate if the associated template use variables from relationships"""
26
+ return len(self.relationships) > 0
27
+
28
+ def get_hash(self) -> str:
29
+ return hashlib.md5("::".join(self.hfid).encode(), usedforsecurity=False).hexdigest()
30
+
31
+
32
+ @dataclass
33
+ class RelationshipIdentifier:
34
+ kind: str
35
+ hfid: list[str]
36
+ filter_key: str
37
+
38
+ def __hash__(self) -> int:
39
+ return hash(f"{self.kind}::{'::'.join(self.hfid)}::{self.filter_key}")
40
+
41
+
42
+ @dataclass
43
+ class RelationshipTriggers:
44
+ attributes: dict[str, set[RelationshipIdentifier]] = field(default_factory=dict)
45
+
46
+
47
+ class HFIDs:
48
+ def __init__(
49
+ self,
50
+ node_level_hfids: dict[str, HFIDDefinition] | None = None,
51
+ relationship_triggers: dict[str, RelationshipTriggers] | None = None,
52
+ ) -> None:
53
+ self._node_level_hfids: dict[str, HFIDDefinition] = node_level_hfids or {}
54
+ self._relationship_triggers: dict[str, RelationshipTriggers] = relationship_triggers or {}
55
+
56
+ def duplicate(self) -> HFIDs:
57
+ return self.__class__(
58
+ node_level_hfids=deepcopy(self._node_level_hfids),
59
+ relationship_triggers=deepcopy(self._relationship_triggers),
60
+ )
61
+
62
+ def register_hfid_schema_path(self, kind: str, schema_path: SchemaAttributePath, hfid: list[str]) -> None:
63
+ """Register HFID using the schema path of each impacted schema path in use."""
64
+ if kind not in self._node_level_hfids:
65
+ self._node_level_hfids[kind] = HFIDDefinition(hfid=hfid)
66
+ if schema_path.is_type_attribute:
67
+ self._node_level_hfids[kind].attributes.add(schema_path.active_attribute_schema.name)
68
+ elif schema_path.is_type_relationship and schema_path.related_schema:
69
+ self._node_level_hfids[kind].relationships.add(schema_path.active_relationship_schema.name)
70
+ if schema_path.related_schema.kind not in self._relationship_triggers:
71
+ self._relationship_triggers[schema_path.related_schema.kind] = RelationshipTriggers()
72
+ if (
73
+ schema_path.active_attribute_schema.name
74
+ not in self._relationship_triggers[schema_path.related_schema.kind].attributes
75
+ ):
76
+ self._relationship_triggers[schema_path.related_schema.kind].attributes[
77
+ schema_path.active_attribute_schema.name
78
+ ] = set()
79
+ self._relationship_triggers[schema_path.related_schema.kind].attributes[
80
+ schema_path.active_attribute_schema.name
81
+ ].add(
82
+ RelationshipIdentifier(
83
+ kind=kind, filter_key=f"{schema_path.active_relationship_schema.name}__ids", hfid=hfid
84
+ )
85
+ )
86
+
87
+ def targets_node(self, kind: str) -> bool:
88
+ """Indicates if there is a human_friendly_id defined for the targeted node"""
89
+ return kind in self._node_level_hfids
90
+
91
+ def get_node_definition(self, kind: str) -> HFIDDefinition:
92
+ """Return node kinds together with their template definitions."""
93
+ return self._node_level_hfids[kind]
94
+
95
+ def get_template_nodes(self) -> dict[str, HFIDDefinition]:
96
+ """Return node kinds together with their template definitions."""
97
+ return self._node_level_hfids
98
+
99
+ def get_related_trigger_nodes(self) -> dict[str, RelationshipTriggers]:
100
+ """Return node kinds that other nodes use within their templates for display_labels."""
101
+ return self._relationship_triggers
102
+
103
+ def get_related_definition(self, related_kind: str, target_kind: str) -> HFIDDefinition:
104
+ relationship_trigger = self._relationship_triggers[related_kind]
105
+ for applicable_kinds in relationship_trigger.attributes.values():
106
+ for relationship_identifier in applicable_kinds:
107
+ if target_kind == relationship_identifier.kind:
108
+ template_label = self.get_node_definition(kind=target_kind)
109
+ template_label.filter_key = relationship_identifier.filter_key
110
+ return template_label
111
+
112
+ raise ValueError(
113
+ f"Unable to find registered template for {target_kind} registered on related node {related_kind}"
114
+ )
@@ -49,7 +49,7 @@ class AggregatedConstraintChecker:
49
49
  node_display_label = None
50
50
  display_label = None
51
51
  if node:
52
- node_display_label = await node.render_display_label(db=self.db)
52
+ node_display_label = await node.get_display_label(db=self.db)
53
53
  if node_display_label:
54
54
  if request.node_schema.display_labels and node:
55
55
  display_label = f"Node {node_display_label} ({node.get_kind()}: {path.node_id})"