infrahub-server 1.5.0b0__py3-none-any.whl → 1.5.0b1__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 (104) hide show
  1. infrahub/actions/tasks.py +8 -0
  2. infrahub/api/diff/diff.py +1 -1
  3. infrahub/cli/db.py +24 -0
  4. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  5. infrahub/core/attribute.py +3 -3
  6. infrahub/core/branch/tasks.py +2 -1
  7. infrahub/core/changelog/models.py +4 -12
  8. infrahub/core/constants/infrahubkind.py +1 -0
  9. infrahub/core/diff/model/path.py +4 -0
  10. infrahub/core/diff/payload_builder.py +1 -1
  11. infrahub/core/graph/__init__.py +1 -1
  12. infrahub/core/ipam/utilization.py +1 -1
  13. infrahub/core/manager.py +6 -3
  14. infrahub/core/migrations/graph/__init__.py +4 -0
  15. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
  16. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
  17. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  18. infrahub/core/migrations/shared.py +5 -6
  19. infrahub/core/node/__init__.py +142 -40
  20. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  21. infrahub/core/node/node_property_attribute.py +230 -0
  22. infrahub/core/node/standard.py +1 -1
  23. infrahub/core/protocols.py +7 -1
  24. infrahub/core/query/node.py +14 -1
  25. infrahub/core/registry.py +2 -2
  26. infrahub/core/relationship/constraints/count.py +1 -1
  27. infrahub/core/relationship/model.py +1 -1
  28. infrahub/core/schema/basenode_schema.py +42 -2
  29. infrahub/core/schema/definitions/core/__init__.py +2 -0
  30. infrahub/core/schema/definitions/core/generator.py +2 -0
  31. infrahub/core/schema/definitions/core/group.py +16 -2
  32. infrahub/core/schema/definitions/internal.py +14 -1
  33. infrahub/core/schema/generated/base_node_schema.py +6 -1
  34. infrahub/core/schema/node_schema.py +5 -2
  35. infrahub/core/schema/schema_branch.py +134 -0
  36. infrahub/core/schema/schema_branch_display.py +123 -0
  37. infrahub/core/schema/schema_branch_hfid.py +114 -0
  38. infrahub/core/validators/aggregated_checker.py +1 -1
  39. infrahub/core/validators/determiner.py +12 -1
  40. infrahub/core/validators/relationship/peer.py +1 -1
  41. infrahub/core/validators/tasks.py +1 -1
  42. infrahub/display_labels/__init__.py +0 -0
  43. infrahub/display_labels/gather.py +48 -0
  44. infrahub/display_labels/models.py +240 -0
  45. infrahub/display_labels/tasks.py +186 -0
  46. infrahub/display_labels/triggers.py +22 -0
  47. infrahub/events/group_action.py +1 -1
  48. infrahub/events/node_action.py +1 -1
  49. infrahub/generators/constants.py +7 -0
  50. infrahub/generators/models.py +7 -0
  51. infrahub/generators/tasks.py +31 -15
  52. infrahub/git/integrator.py +22 -14
  53. infrahub/graphql/analyzer.py +1 -1
  54. infrahub/graphql/mutations/display_label.py +111 -0
  55. infrahub/graphql/mutations/generator.py +25 -7
  56. infrahub/graphql/mutations/hfid.py +118 -0
  57. infrahub/graphql/mutations/relationship.py +2 -2
  58. infrahub/graphql/mutations/resource_manager.py +2 -2
  59. infrahub/graphql/mutations/schema.py +5 -5
  60. infrahub/graphql/queries/resource_manager.py +1 -1
  61. infrahub/graphql/resolvers/resolver.py +2 -0
  62. infrahub/graphql/schema.py +4 -0
  63. infrahub/groups/tasks.py +1 -1
  64. infrahub/hfid/__init__.py +0 -0
  65. infrahub/hfid/gather.py +48 -0
  66. infrahub/hfid/models.py +240 -0
  67. infrahub/hfid/tasks.py +185 -0
  68. infrahub/hfid/triggers.py +22 -0
  69. infrahub/lock.py +15 -4
  70. infrahub/middleware.py +26 -1
  71. infrahub/proposed_change/tasks.py +10 -1
  72. infrahub/server.py +16 -3
  73. infrahub/services/__init__.py +8 -5
  74. infrahub/trigger/catalogue.py +4 -0
  75. infrahub/trigger/models.py +2 -0
  76. infrahub/trigger/tasks.py +3 -0
  77. infrahub/workflows/catalogue.py +72 -0
  78. infrahub/workflows/initialization.py +16 -0
  79. infrahub_sdk/checks.py +1 -1
  80. infrahub_sdk/ctl/cli_commands.py +2 -0
  81. infrahub_sdk/ctl/generator.py +4 -0
  82. infrahub_sdk/ctl/graphql.py +184 -0
  83. infrahub_sdk/ctl/schema.py +6 -2
  84. infrahub_sdk/generator.py +7 -1
  85. infrahub_sdk/graphql/__init__.py +12 -0
  86. infrahub_sdk/graphql/constants.py +1 -0
  87. infrahub_sdk/graphql/plugin.py +85 -0
  88. infrahub_sdk/graphql/query.py +77 -0
  89. infrahub_sdk/{graphql.py → graphql/renderers.py} +81 -73
  90. infrahub_sdk/graphql/utils.py +40 -0
  91. infrahub_sdk/protocols.py +14 -0
  92. infrahub_sdk/schema/__init__.py +38 -0
  93. infrahub_sdk/schema/repository.py +8 -0
  94. infrahub_sdk/spec/object.py +84 -10
  95. infrahub_sdk/spec/range_expansion.py +1 -1
  96. infrahub_sdk/transforms.py +1 -1
  97. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +5 -4
  98. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +104 -79
  99. infrahub_testcontainers/container.py +1 -1
  100. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  101. infrahub_testcontainers/docker-compose.test.yml +1 -1
  102. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
  103. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
  104. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
@@ -10,8 +10,7 @@ from infrahub.core.path import SchemaPath # noqa: TC001
10
10
  from infrahub.core.query import Query # noqa: TC001
11
11
  from infrahub.core.schema import (
12
12
  AttributeSchema,
13
- GenericSchema,
14
- NodeSchema,
13
+ MainSchemaTypes,
15
14
  RelationshipSchema,
16
15
  SchemaRoot,
17
16
  internal_schema,
@@ -43,8 +42,8 @@ class SchemaMigration(BaseModel):
43
42
  name: str = Field(..., description="Name of the migration")
44
43
  queries: Sequence[type[MigrationQuery]] = Field(..., description="List of queries to execute for this migration")
45
44
 
46
- new_node_schema: NodeSchema | GenericSchema | None = None
47
- previous_node_schema: NodeSchema | GenericSchema | None = None
45
+ new_node_schema: MainSchemaTypes | None = None
46
+ previous_node_schema: MainSchemaTypes | None = None
48
47
  schema_path: SchemaPath
49
48
 
50
49
  async def execute_pre_queries(
@@ -91,13 +90,13 @@ class SchemaMigration(BaseModel):
91
90
  return result
92
91
 
93
92
  @property
94
- def new_schema(self) -> NodeSchema | GenericSchema:
93
+ def new_schema(self) -> MainSchemaTypes:
95
94
  if self.new_node_schema:
96
95
  return self.new_node_schema
97
96
  raise ValueError("new_node_schema hasn't been initialized")
98
97
 
99
98
  @property
100
- def previous_schema(self) -> NodeSchema | GenericSchema:
99
+ def previous_schema(self) -> MainSchemaTypes:
101
100
  if self.previous_node_schema:
102
101
  return self.previous_node_schema
103
102
  raise ValueError("previous_node_schema hasn't been initialized")
@@ -47,6 +47,7 @@ from ..query.relationship import RelationshipDeleteAllQuery
47
47
  from ..relationship import RelationshipManager
48
48
  from ..utils import update_relationships_to
49
49
  from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
50
+ from .node_property_attribute import DisplayLabel, HumanFriendlyIdentifier
50
51
 
51
52
  if TYPE_CHECKING:
52
53
  from typing_extensions import Self
@@ -79,6 +80,29 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
79
80
  _meta.default_filter = default_filter
80
81
  super().__init_subclass_with_meta__(_meta=_meta, **options)
81
82
 
83
+ def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
84
+ self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
85
+ self._branch: Branch = branch
86
+ self._at: Timestamp = at
87
+ self._existing: bool = False
88
+
89
+ self._updated_at: Timestamp | None = None
90
+ self.id: str = None
91
+ self.db_id: str = None
92
+
93
+ self._source: Node | None = None
94
+ self._owner: Node | None = None
95
+ self._is_protected: bool = None
96
+ self._computed_jinja2_attributes: list[str] = []
97
+
98
+ self._display_label: DisplayLabel | None = None
99
+ self._human_friendly_id: HumanFriendlyIdentifier | None = None
100
+
101
+ # Lists of attributes and relationships names
102
+ self._attributes: list[str] = []
103
+ self._relationships: list[str] = []
104
+ self._node_changelog: NodeChangelog | None = None
105
+
82
106
  def get_schema(self) -> NonGenericSchemaTypes:
83
107
  return self._schema
84
108
 
@@ -126,11 +150,14 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
126
150
  if not self._schema.human_friendly_id:
127
151
  return None
128
152
 
129
- hfid_values = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
153
+ hfid_values: list[str] | None = None
154
+ if self._human_friendly_id:
155
+ hfid_values = self._human_friendly_id.get_value(node=self, at=self._at)
156
+ if not hfid_values:
157
+ hfid_values = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
158
+
130
159
  hfid = [value for value in hfid_values if value is not None]
131
- if include_kind:
132
- return [self.get_kind()] + hfid
133
- return hfid
160
+ return [self.get_kind()] + hfid if include_kind else hfid
134
161
 
135
162
  async def get_hfid_as_string(self, db: InfrahubDatabase, include_kind: bool = False) -> str | None:
136
163
  """Return the Human friendly id of the node in string format separated with a dunder (__) ."""
@@ -139,6 +166,37 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
139
166
  return None
140
167
  return "__".join(hfid)
141
168
 
169
+ def has_human_friendly_id(self) -> bool:
170
+ return self._human_friendly_id is not None
171
+
172
+ async def add_human_friendly_id(self, db: InfrahubDatabase) -> None:
173
+ if not self._schema.human_friendly_id or self._human_friendly_id:
174
+ return
175
+
176
+ self._human_friendly_id = HumanFriendlyIdentifier(
177
+ node_schema=self._schema, template=self._schema.human_friendly_id
178
+ )
179
+ await self._human_friendly_id.compute(db=db, node=self)
180
+
181
+ async def get_display_label(self, db: InfrahubDatabase) -> str:
182
+ if self._display_label:
183
+ if isinstance(self._display_label._value, str):
184
+ return self._display_label._value
185
+ if self._display_label._value:
186
+ return self._display_label._value.value
187
+
188
+ return await self.render_display_label(db=db)
189
+
190
+ def has_display_label(self) -> bool:
191
+ return self._display_label is not None
192
+
193
+ async def add_display_label(self, db: InfrahubDatabase) -> None:
194
+ if not self._schema.display_label or self._display_label:
195
+ return
196
+
197
+ self._display_label = DisplayLabel(node_schema=self._schema, template=self._schema.display_label)
198
+ await self._display_label.compute(db=db, node=self)
199
+
142
200
  async def get_path_value(self, db: InfrahubDatabase, path: str) -> str:
143
201
  schema_path = self._schema.parse_schema_path(
144
202
  path=path, schema=db.schema.get_schema_branch(name=self._branch.name)
@@ -197,30 +255,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
197
255
  return self._branch
198
256
 
199
257
  def __repr__(self) -> str:
200
- if not self._existing:
201
- return f"{self.get_kind()}(ID: {str(self.id)})[NEW]"
202
-
203
- return f"{self.get_kind()}(ID: {str(self.id)})"
204
-
205
- def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
206
- self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
207
- self._branch: Branch = branch
208
- self._at: Timestamp = at
209
- self._existing: bool = False
210
-
211
- self._updated_at: Timestamp | None = None
212
- self.id: str = None
213
- self.db_id: str = None
214
-
215
- self._source: Node | None = None
216
- self._owner: Node | None = None
217
- self._is_protected: bool = None
218
- self._computed_jinja2_attributes: list[str] = []
219
-
220
- # Lists of attributes and relationships names
221
- self._attributes: list[str] = []
222
- self._relationships: list[str] = []
223
- self._node_changelog: NodeChangelog | None = None
258
+ v = f"{self.get_kind()}(ID: {str(self.id)})"
259
+ return v if self._existing else f"{v}[NEW]"
224
260
 
225
261
  @property
226
262
  def node_changelog(self) -> NodeChangelog:
@@ -733,12 +769,26 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
733
769
  )
734
770
  self._updated_at = Timestamp(updated_at)
735
771
 
772
+ if not self._schema.is_schema_node:
773
+ if hfid := kwargs.pop("human_friendly_id", None):
774
+ self._human_friendly_id = HumanFriendlyIdentifier(
775
+ node_schema=self._schema, template=self._schema.human_friendly_id, value=hfid
776
+ )
777
+ if display_label := kwargs.pop("display_label", None):
778
+ self._display_label = DisplayLabel(
779
+ node_schema=self._schema, template=self._schema.display_label, value=display_label
780
+ )
781
+
736
782
  await self._process_fields(db=db, fields=kwargs)
737
783
  return self
738
784
 
739
785
  async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> NodeChangelog:
740
786
  create_at = Timestamp(at)
741
787
 
788
+ if not self._schema.is_schema_node:
789
+ await self.add_human_friendly_id(db=db)
790
+ await self.add_display_label(db=db)
791
+
742
792
  query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at)
743
793
  await query.execute(db=db)
744
794
 
@@ -750,6 +800,13 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
750
800
  new_ids = query.get_ids()
751
801
  node_changelog = NodeChangelog(node_id=self.get_id(), node_kind=self.get_kind(), display_label="")
752
802
 
803
+ if self._human_friendly_id:
804
+ node_changelog.create_attribute(
805
+ attribute=self._human_friendly_id.get_node_attribute(node=self, at=create_at)
806
+ )
807
+ if self._display_label:
808
+ node_changelog.create_attribute(attribute=self._display_label.get_node_attribute(node=self, at=create_at))
809
+
753
810
  # Go over the list of Attribute and assign the new IDs one by one
754
811
  for name in self._attributes:
755
812
  attr: BaseAttribute = getattr(self, name)
@@ -762,12 +819,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
762
819
  relm: RelationshipManager = getattr(self, name)
763
820
  for rel in relm._relationships:
764
821
  identifier = f"{rel.schema.identifier}::{rel.peer_id}"
765
-
766
822
  rel.id, rel.db_id = new_ids[identifier]
767
-
768
823
  node_changelog.create_relationship(relationship=rel)
769
824
 
770
- node_changelog.display_label = await self.render_display_label(db=db)
825
+ node_changelog.display_label = await self.get_display_label(db=db)
771
826
  return node_changelog
772
827
 
773
828
  async def _update(
@@ -803,19 +858,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
803
858
  if parent := await rel.get_parent(db=db):
804
859
  node_changelog.add_parent_from_relationship(parent=parent)
805
860
 
806
- node_changelog.display_label = await self.render_display_label(db=db)
861
+ # Update the HFID if one of its variables is being updated
862
+ if self._human_friendly_id and (
863
+ (fields and "human_friendly_id" in fields) or self._human_friendly_id.needs_update(fields=fields)
864
+ ):
865
+ await self._human_friendly_id.compute(db=db, node=self)
866
+ updated_attribute = await self._human_friendly_id.get_node_attribute(node=self, at=update_at).save(
867
+ at=update_at, db=db
868
+ )
869
+ if updated_attribute:
870
+ node_changelog.add_attribute(attribute=updated_attribute)
871
+
872
+ # Update the display label if one of its variables is being updated
873
+ if self._display_label and (
874
+ (fields and "display_label" in fields) or self._display_label.needs_update(fields=fields)
875
+ ):
876
+ await self._display_label.compute(db=db, node=self)
877
+ self._display_label.get_node_attribute(node=self, at=update_at).get_create_data(node_schema=self._schema)
878
+ updated_attribute = await self._display_label.get_node_attribute(node=self, at=update_at).save(
879
+ at=update_at, db=db
880
+ )
881
+ if updated_attribute:
882
+ node_changelog.add_attribute(attribute=updated_attribute)
883
+
884
+ node_changelog.display_label = await self.get_display_label(db=db)
807
885
  return node_changelog
808
886
 
809
887
  async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> Self:
810
888
  """Create or Update the Node in the database."""
811
-
812
889
  save_at = Timestamp(at)
813
890
 
814
891
  if self._existing:
815
892
  self._node_changelog = await self._update(at=save_at, db=db, fields=fields)
816
- return self
893
+ else:
894
+ self._node_changelog = await self._create(at=save_at, db=db)
817
895
 
818
- self._node_changelog = await self._create(at=save_at, db=db)
819
896
  return self
820
897
 
821
898
  async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
@@ -824,13 +901,24 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
824
901
  delete_at = Timestamp(at)
825
902
 
826
903
  node_changelog = NodeChangelog(
827
- node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.render_display_label(db=db)
904
+ node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.get_display_label(db=db)
828
905
  )
829
906
  # Go over the list of Attribute and update them one by one
830
907
  for name in self._attributes:
831
908
  attr: BaseAttribute = getattr(self, name)
832
- deleted_attribute = await attr.delete(at=delete_at, db=db)
833
- if deleted_attribute:
909
+ if deleted_attribute := await attr.delete(at=delete_at, db=db):
910
+ node_changelog.add_attribute(attribute=deleted_attribute)
911
+
912
+ if self._human_friendly_id:
913
+ if deleted_attribute := await self._human_friendly_id.get_node_attribute(node=self, at=delete_at).delete(
914
+ at=delete_at, db=db
915
+ ):
916
+ node_changelog.add_attribute(attribute=deleted_attribute)
917
+
918
+ if self._display_label:
919
+ if deleted_attribute := await self._display_label.get_node_attribute(node=self, at=delete_at).delete(
920
+ at=delete_at, db=db
921
+ ):
834
922
  node_changelog.add_attribute(attribute=deleted_attribute)
835
923
 
836
924
  branch = self.get_branch_based_on_support_type()
@@ -898,7 +986,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
898
986
  continue
899
987
 
900
988
  if field_name == "display_label":
901
- response[field_name] = await self.render_display_label(db=db)
989
+ response[field_name] = await self.get_display_label(db=db)
902
990
  continue
903
991
 
904
992
  if field_name == "hfid":
@@ -1001,6 +1089,20 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
1001
1089
  return repr(self)
1002
1090
  return display_label.strip()
1003
1091
 
1092
+ async def set_human_friendly_id(self, value: list[str] | None) -> None:
1093
+ """Set the human friendly ID of this node if one is set. `save()` must be called to commit the change in the database."""
1094
+ if self._human_friendly_id is None:
1095
+ return
1096
+
1097
+ self._human_friendly_id.set_value(value=value, manually_assigned=True)
1098
+
1099
+ async def set_display_label(self, value: str | None) -> None:
1100
+ """Set the display label of this node if one is set. `save()` must be called to commit the change in the database."""
1101
+ if self._display_label is None:
1102
+ return
1103
+
1104
+ self._display_label.set_value(value=value, manually_assigned=True)
1105
+
1004
1106
  def _get_parent_relationship_name(self) -> str | None:
1005
1107
  """Return the name of the parent relationship is one is present"""
1006
1108
  for relationship in self._schema.relationships:
@@ -1010,7 +1112,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
1010
1112
  return None
1011
1113
 
1012
1114
  async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
1013
- object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
1115
+ object_template: RelationshipManager | None = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
1014
1116
  return (
1015
1117
  await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
1016
1118
  )
@@ -29,7 +29,9 @@ class NodeAttributeUniquenessConstraint(NodeConstraintInterface):
29
29
  attr = getattr(node, unique_attr.name)
30
30
  if unique_attr.inherited:
31
31
  for generic_parent_schema_name in node_schema.inherit_from:
32
- generic_parent_schema = self.db.schema.get(generic_parent_schema_name, branch=self.branch)
32
+ generic_parent_schema = self.db.schema.get(
33
+ generic_parent_schema_name, branch=self.branch, duplicate=False
34
+ )
33
35
  parent_attr = generic_parent_schema.get_attribute_or_none(unique_attr.name)
34
36
  if parent_attr is None:
35
37
  continue
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+
7
+ from infrahub_sdk.template import Jinja2Template
8
+
9
+ from infrahub.core.query.node import AttributeFromDB
10
+ from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
11
+
12
+ from ..attribute import BaseAttribute, ListAttributeOptional, StringOptional
13
+
14
+ if TYPE_CHECKING:
15
+ from infrahub.core.node import Node
16
+ from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
17
+ from infrahub.core.schema.attribute_schema import AttributeSchema
18
+ from infrahub.core.timestamp import Timestamp
19
+ from infrahub.database import InfrahubDatabase
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class NodePropertyAttribute(Generic[T]):
25
+ """A node property attribute is a construct that seats between a property and an attribute.
26
+
27
+ View it as a property, set at the node level but stored in the database as an attribute. It usually is something computed from other components of
28
+ a node, such as its attributes and its relationships.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
34
+ template: T | None,
35
+ value: AttributeFromDB | T | None = None,
36
+ ) -> None:
37
+ self.node_schema = node_schema
38
+
39
+ self.node_attributes: list[str] = []
40
+ self.node_relationships: list[str] = []
41
+
42
+ self.template = template
43
+ self._value = value
44
+ self._manually_assigned = False
45
+
46
+ self.schema: AttributeSchema
47
+
48
+ self.analyze_variables()
49
+
50
+ def needs_update(self, fields: list[str] | None) -> bool:
51
+ """Tell if this node property attribute must be recomputed given a list of updated fields of a node."""
52
+ if self._manually_assigned or not fields:
53
+ return True
54
+ for field in fields:
55
+ if field in self.node_attributes or field in self.node_relationships:
56
+ return True
57
+
58
+ return False
59
+
60
+ @property
61
+ def attribute_value(self) -> AttributeFromDB | dict[str, T | None]:
62
+ if isinstance(self._value, AttributeFromDB):
63
+ return self._value
64
+ return {"value": self._value}
65
+
66
+ def set_value(self, value: T | None, manually_assigned: bool = False) -> None:
67
+ """Force the value of the node property attribute to the given one."""
68
+ if isinstance(self._value, AttributeFromDB):
69
+ self._value.value = value
70
+ else:
71
+ self._value = value
72
+
73
+ if manually_assigned:
74
+ self._manually_assigned = True
75
+
76
+ def get_value(self, node: Node, at: Timestamp) -> T | None:
77
+ if isinstance(self._value, AttributeFromDB):
78
+ attr = self.get_node_attribute(node=node, at=at)
79
+ return attr.value # type: ignore
80
+
81
+ return self._value
82
+
83
+ @abstractmethod
84
+ def analyze_variables(self) -> None: ...
85
+
86
+ @abstractmethod
87
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None: ...
88
+
89
+ @abstractmethod
90
+ def get_node_attribute(self, node: Node, at: Timestamp) -> BaseAttribute: ...
91
+
92
+
93
+ class DisplayLabel(NodePropertyAttribute[str]):
94
+ def __init__(
95
+ self,
96
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
97
+ template: str | None,
98
+ value: AttributeFromDB | str | None = None,
99
+ ) -> None:
100
+ super().__init__(node_schema=node_schema, template=template, value=value)
101
+
102
+ self.schema = node_schema.get_attribute(name="display_label")
103
+
104
+ @property
105
+ def is_jinja2_template(self) -> bool:
106
+ if self.template is None:
107
+ return False
108
+
109
+ return any(c in self.template for c in "{}")
110
+
111
+ def _analyze_plain_value(self) -> None:
112
+ if self.template is None or "__" not in self.template:
113
+ return
114
+
115
+ items = self.template.split("__", maxsplit=1)
116
+ if items[0] not in self.node_schema.attribute_names:
117
+ raise ValueError(f"{items[0]} is not an attribute of {self.node_schema.kind}")
118
+
119
+ self.node_attributes.append(items[0])
120
+
121
+ def _analyze_jinja2_value(self) -> None:
122
+ if self.template is None or not self.is_jinja2_template:
123
+ return
124
+
125
+ tpl = Jinja2Template(template=self.template)
126
+ for variable in tpl.get_variables():
127
+ items = variable.split("__", maxsplit=1)
128
+ if items[0] in self.node_schema.attribute_names:
129
+ self.node_attributes.append(items[0])
130
+ elif items[0] in self.node_schema.relationship_names:
131
+ self.node_relationships.append(items[0])
132
+ else:
133
+ raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
134
+
135
+ def analyze_variables(self) -> None:
136
+ """Look at variables used in the display label and record attributes and relationships required to compute it."""
137
+ if not self.is_jinja2_template:
138
+ self._analyze_plain_value()
139
+ else:
140
+ self._analyze_jinja2_value()
141
+
142
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None:
143
+ """Update the display label value by recomputing it from the template."""
144
+ if self.template is None or self._manually_assigned:
145
+ return
146
+
147
+ if node.get_schema() != self.node_schema:
148
+ raise ValueError(
149
+ f"display_label for schema {self.node_schema.kind} cannot be rendered for node {node.get_schema().kind} {node.id}"
150
+ )
151
+
152
+ if not self.is_jinja2_template:
153
+ path_value = await node.get_path_value(db=db, path=self.template)
154
+ # Use .value for enum to keep compat with old style display label
155
+ self.set_value(value=str(path_value if not isinstance(path_value, Enum) else path_value.value))
156
+ return
157
+
158
+ jinja2_template = Jinja2Template(template=self.template)
159
+
160
+ variables: dict[str, Any] = {}
161
+ for variable in jinja2_template.get_variables():
162
+ variables[variable] = await node.get_path_value(db=db, path=variable)
163
+
164
+ self.set_value(value=await jinja2_template.render(variables=variables))
165
+
166
+ def get_node_attribute(self, node: Node, at: Timestamp) -> StringOptional:
167
+ """Return a node attribute that can be stored in the database for this display label and node."""
168
+ return StringOptional(
169
+ name="display_label",
170
+ schema=self.schema,
171
+ branch=node.get_branch(),
172
+ at=at,
173
+ node=node,
174
+ data=self.attribute_value,
175
+ )
176
+
177
+
178
+ class HumanFriendlyIdentifier(NodePropertyAttribute[list[str]]):
179
+ def __init__(
180
+ self,
181
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
182
+ template: list[str] | None,
183
+ value: AttributeFromDB | list[str] | None = None,
184
+ ) -> None:
185
+ super().__init__(node_schema=node_schema, template=template, value=value)
186
+
187
+ self.schema = node_schema.get_attribute(name="human_friendly_id")
188
+
189
+ def _analyze_single_variable(self, value: str) -> None:
190
+ items = value.split("__", maxsplit=1)
191
+ if items[0] in self.node_schema.attribute_names:
192
+ self.node_attributes.append(items[0])
193
+ elif items[0] in self.node_schema.relationship_names:
194
+ self.node_relationships.append(items[0])
195
+ else:
196
+ raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
197
+
198
+ def analyze_variables(self) -> None:
199
+ """Look at variables used in the HFID and record attributes and relationships required to compute it."""
200
+ for item in self.template or []:
201
+ self._analyze_single_variable(value=item)
202
+
203
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None:
204
+ """Update the HFID value by recomputing it from the template."""
205
+ if self.template is None or self._manually_assigned:
206
+ return
207
+
208
+ if node.get_schema() != self.node_schema:
209
+ raise ValueError(
210
+ f"human_friendly_id for schema {self.node_schema.kind} cannot be computed for node {node.get_schema().kind} {node.id}"
211
+ )
212
+
213
+ value: list[str] = []
214
+ for path in self.template:
215
+ path_value = await node.get_path_value(db=db, path=path)
216
+ # Use .value for enum to be consistent with display label
217
+ value.append(path_value if not isinstance(path_value, Enum) else path_value.value)
218
+
219
+ self.set_value(value=value)
220
+
221
+ def get_node_attribute(self, node: Node, at: Timestamp) -> ListAttributeOptional:
222
+ """Return a node attribute that can be stored in the database for this HFID and node."""
223
+ return ListAttributeOptional(
224
+ name="human_friendly_id",
225
+ schema=self.schema,
226
+ branch=node.get_branch(),
227
+ at=at,
228
+ node=node,
229
+ data=self.attribute_value,
230
+ )
@@ -111,7 +111,7 @@ class StandardNode(BaseModel):
111
111
  node = result.get("n")
112
112
 
113
113
  self.id = node.element_id
114
- self.uuid = node["uuid"]
114
+ self.uuid = UUID(node["uuid"])
115
115
 
116
116
  return True
117
117
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from .protocols_base import CoreNode
7
+ from infrahub.core.protocols_base import CoreNode
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from enum import Enum
@@ -350,6 +350,10 @@ class CoreGeneratorAction(CoreAction):
350
350
  generator: RelationshipManager
351
351
 
352
352
 
353
+ class CoreGeneratorAwareGroup(CoreGroup):
354
+ pass
355
+
356
+
353
357
  class CoreGeneratorCheck(CoreCheck):
354
358
  instance: String
355
359
 
@@ -361,6 +365,8 @@ class CoreGeneratorDefinition(CoreTaskTarget):
361
365
  file_path: String
362
366
  class_name: String
363
367
  convert_query_response: BooleanOptional
368
+ execute_in_proposed_change: BooleanOptional
369
+ execute_after_merge: BooleanOptional
364
370
  query: RelationshipManager
365
371
  repository: RelationshipManager
366
372
  targets: RelationshipManager
@@ -142,9 +142,22 @@ class NodeCreateAllQuery(NodeQuery):
142
142
  attributes_ipnetwork: list[AttributeCreateData] = []
143
143
  attributes_indexed: list[AttributeCreateData] = []
144
144
 
145
+ if self.node.has_display_label():
146
+ attributes_indexed.append(
147
+ self.node._display_label.get_node_attribute(node=self.node, at=at).get_create_data(
148
+ node_schema=self.node.get_schema()
149
+ )
150
+ )
151
+ if self.node.has_human_friendly_id():
152
+ attributes_indexed.append(
153
+ self.node._human_friendly_id.get_node_attribute(node=self.node, at=at).get_create_data(
154
+ node_schema=self.node.get_schema()
155
+ )
156
+ )
157
+
145
158
  for attr_name in self.node._attributes:
146
159
  attr: BaseAttribute = getattr(self.node, attr_name)
147
- attr_data = attr.get_create_data()
160
+ attr_data = attr.get_create_data(node_schema=self.node.get_schema())
148
161
  node_type = attr.get_db_node_type()
149
162
 
150
163
  if AttributeDBNodeType.IPHOST in node_type:
infrahub/core/registry.py CHANGED
@@ -113,8 +113,8 @@ class Registry:
113
113
  return True
114
114
  return False
115
115
 
116
- def get_node_schema(self, name: str, branch: Branch | str | None = None) -> NodeSchema:
117
- return self.schema.get_node_schema(name=name, branch=branch)
116
+ def get_node_schema(self, name: str, branch: Branch | str | None = None, duplicate: bool = False) -> NodeSchema:
117
+ return self.schema.get_node_schema(name=name, branch=branch, duplicate=duplicate)
118
118
 
119
119
  def get_data_type(self, name: str) -> type[InfrahubDataType]:
120
120
  if name not in self.data_type:
@@ -40,7 +40,7 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
40
40
  # peer_ids_present_database_only:
41
41
  # relationship to be deleted, need to check if the schema on the other side has a min_count defined
42
42
  # TODO see how to manage Generic node
43
- peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch)
43
+ peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch, duplicate=False)
44
44
  peer_rels = peer_schema.get_relationships_by_identifier(id=relm.schema.get_identifier())
45
45
  if not peer_rels:
46
46
  return
@@ -459,7 +459,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
459
459
  self.set_peer(value=peer)
460
460
 
461
461
  if not self.peer_id and self.peer_hfid:
462
- peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch)
462
+ peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch, duplicate=False)
463
463
  kind = (
464
464
  self.data["kind"]
465
465
  if isinstance(self.data, dict) and "kind" in self.data and peer_schema.is_generic_schema