infrahub-server 1.4.10__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 (178) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/query.py +2 -0
  5. infrahub/api/schema.py +3 -0
  6. infrahub/auth.py +5 -5
  7. infrahub/cli/db.py +26 -2
  8. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  9. infrahub/config.py +7 -2
  10. infrahub/core/attribute.py +25 -22
  11. infrahub/core/branch/models.py +2 -2
  12. infrahub/core/branch/needs_rebase_status.py +11 -0
  13. infrahub/core/branch/tasks.py +4 -3
  14. infrahub/core/changelog/models.py +4 -12
  15. infrahub/core/constants/__init__.py +1 -0
  16. infrahub/core/constants/infrahubkind.py +1 -0
  17. infrahub/core/convert_object_type/object_conversion.py +201 -0
  18. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  19. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  20. infrahub/core/diff/model/path.py +4 -0
  21. infrahub/core/diff/payload_builder.py +1 -1
  22. infrahub/core/diff/query/artifact.py +1 -1
  23. infrahub/core/graph/__init__.py +1 -1
  24. infrahub/core/initialization.py +2 -2
  25. infrahub/core/ipam/utilization.py +1 -1
  26. infrahub/core/manager.py +9 -84
  27. infrahub/core/migrations/graph/__init__.py +6 -0
  28. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  29. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
  30. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
  31. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  32. infrahub/core/migrations/shared.py +5 -6
  33. infrahub/core/node/__init__.py +165 -42
  34. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  35. infrahub/core/node/create.py +67 -35
  36. infrahub/core/node/lock_utils.py +98 -0
  37. infrahub/core/node/node_property_attribute.py +230 -0
  38. infrahub/core/node/standard.py +1 -1
  39. infrahub/core/property.py +11 -0
  40. infrahub/core/protocols.py +8 -1
  41. infrahub/core/query/attribute.py +27 -15
  42. infrahub/core/query/node.py +61 -185
  43. infrahub/core/query/relationship.py +43 -26
  44. infrahub/core/query/subquery.py +0 -8
  45. infrahub/core/registry.py +2 -2
  46. infrahub/core/relationship/constraints/count.py +1 -1
  47. infrahub/core/relationship/model.py +60 -20
  48. infrahub/core/schema/attribute_schema.py +0 -2
  49. infrahub/core/schema/basenode_schema.py +42 -2
  50. infrahub/core/schema/definitions/core/__init__.py +2 -0
  51. infrahub/core/schema/definitions/core/generator.py +2 -0
  52. infrahub/core/schema/definitions/core/group.py +16 -2
  53. infrahub/core/schema/definitions/core/repository.py +7 -0
  54. infrahub/core/schema/definitions/internal.py +14 -1
  55. infrahub/core/schema/generated/base_node_schema.py +6 -1
  56. infrahub/core/schema/node_schema.py +5 -2
  57. infrahub/core/schema/relationship_schema.py +0 -1
  58. infrahub/core/schema/schema_branch.py +137 -2
  59. infrahub/core/schema/schema_branch_display.py +123 -0
  60. infrahub/core/schema/schema_branch_hfid.py +114 -0
  61. infrahub/core/validators/aggregated_checker.py +1 -1
  62. infrahub/core/validators/determiner.py +12 -1
  63. infrahub/core/validators/relationship/peer.py +1 -1
  64. infrahub/core/validators/tasks.py +1 -1
  65. infrahub/display_labels/__init__.py +0 -0
  66. infrahub/display_labels/gather.py +48 -0
  67. infrahub/display_labels/models.py +240 -0
  68. infrahub/display_labels/tasks.py +186 -0
  69. infrahub/display_labels/triggers.py +22 -0
  70. infrahub/events/group_action.py +1 -1
  71. infrahub/events/node_action.py +1 -1
  72. infrahub/generators/constants.py +7 -0
  73. infrahub/generators/models.py +38 -12
  74. infrahub/generators/tasks.py +34 -16
  75. infrahub/git/base.py +38 -1
  76. infrahub/git/integrator.py +22 -14
  77. infrahub/graphql/analyzer.py +1 -1
  78. infrahub/graphql/api/dependencies.py +2 -4
  79. infrahub/graphql/api/endpoints.py +2 -2
  80. infrahub/graphql/app.py +2 -4
  81. infrahub/graphql/initialization.py +2 -3
  82. infrahub/graphql/manager.py +212 -137
  83. infrahub/graphql/middleware.py +12 -0
  84. infrahub/graphql/mutations/branch.py +11 -0
  85. infrahub/graphql/mutations/computed_attribute.py +110 -3
  86. infrahub/graphql/mutations/convert_object_type.py +34 -13
  87. infrahub/graphql/mutations/display_label.py +111 -0
  88. infrahub/graphql/mutations/generator.py +25 -7
  89. infrahub/graphql/mutations/hfid.py +118 -0
  90. infrahub/graphql/mutations/ipam.py +21 -8
  91. infrahub/graphql/mutations/main.py +37 -153
  92. infrahub/graphql/mutations/profile.py +195 -0
  93. infrahub/graphql/mutations/proposed_change.py +2 -1
  94. infrahub/graphql/mutations/relationship.py +2 -2
  95. infrahub/graphql/mutations/repository.py +22 -83
  96. infrahub/graphql/mutations/resource_manager.py +2 -2
  97. infrahub/graphql/mutations/schema.py +5 -5
  98. infrahub/graphql/mutations/webhook.py +1 -1
  99. infrahub/graphql/queries/resource_manager.py +1 -1
  100. infrahub/graphql/registry.py +173 -0
  101. infrahub/graphql/resolvers/resolver.py +2 -0
  102. infrahub/graphql/schema.py +8 -1
  103. infrahub/groups/tasks.py +1 -1
  104. infrahub/hfid/__init__.py +0 -0
  105. infrahub/hfid/gather.py +48 -0
  106. infrahub/hfid/models.py +240 -0
  107. infrahub/hfid/tasks.py +185 -0
  108. infrahub/hfid/triggers.py +22 -0
  109. infrahub/lock.py +67 -30
  110. infrahub/locks/__init__.py +0 -0
  111. infrahub/locks/tasks.py +37 -0
  112. infrahub/middleware.py +26 -1
  113. infrahub/patch/plan_writer.py +2 -2
  114. infrahub/profiles/__init__.py +0 -0
  115. infrahub/profiles/node_applier.py +101 -0
  116. infrahub/profiles/queries/__init__.py +0 -0
  117. infrahub/profiles/queries/get_profile_data.py +99 -0
  118. infrahub/profiles/tasks.py +63 -0
  119. infrahub/proposed_change/tasks.py +10 -1
  120. infrahub/repositories/__init__.py +0 -0
  121. infrahub/repositories/create_repository.py +113 -0
  122. infrahub/server.py +16 -3
  123. infrahub/services/__init__.py +8 -5
  124. infrahub/tasks/registry.py +6 -4
  125. infrahub/trigger/catalogue.py +4 -0
  126. infrahub/trigger/models.py +2 -0
  127. infrahub/trigger/tasks.py +3 -0
  128. infrahub/webhook/models.py +1 -1
  129. infrahub/workflows/catalogue.py +110 -3
  130. infrahub/workflows/initialization.py +16 -0
  131. infrahub/workflows/models.py +17 -2
  132. infrahub_sdk/branch.py +5 -8
  133. infrahub_sdk/checks.py +1 -1
  134. infrahub_sdk/client.py +364 -84
  135. infrahub_sdk/convert_object_type.py +61 -0
  136. infrahub_sdk/ctl/check.py +2 -3
  137. infrahub_sdk/ctl/cli_commands.py +18 -12
  138. infrahub_sdk/ctl/config.py +8 -2
  139. infrahub_sdk/ctl/generator.py +6 -3
  140. infrahub_sdk/ctl/graphql.py +184 -0
  141. infrahub_sdk/ctl/repository.py +39 -1
  142. infrahub_sdk/ctl/schema.py +18 -3
  143. infrahub_sdk/ctl/utils.py +4 -0
  144. infrahub_sdk/ctl/validate.py +5 -3
  145. infrahub_sdk/diff.py +4 -5
  146. infrahub_sdk/exceptions.py +2 -0
  147. infrahub_sdk/generator.py +7 -1
  148. infrahub_sdk/graphql/__init__.py +12 -0
  149. infrahub_sdk/graphql/constants.py +1 -0
  150. infrahub_sdk/graphql/plugin.py +85 -0
  151. infrahub_sdk/graphql/query.py +77 -0
  152. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  153. infrahub_sdk/graphql/utils.py +40 -0
  154. infrahub_sdk/node/attribute.py +2 -0
  155. infrahub_sdk/node/node.py +28 -20
  156. infrahub_sdk/playback.py +1 -2
  157. infrahub_sdk/protocols.py +54 -6
  158. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  159. infrahub_sdk/pytest_plugin/utils.py +40 -0
  160. infrahub_sdk/repository.py +1 -2
  161. infrahub_sdk/schema/__init__.py +38 -0
  162. infrahub_sdk/schema/main.py +1 -0
  163. infrahub_sdk/schema/repository.py +8 -0
  164. infrahub_sdk/spec/object.py +120 -7
  165. infrahub_sdk/spec/range_expansion.py +118 -0
  166. infrahub_sdk/timestamp.py +18 -6
  167. infrahub_sdk/transforms.py +1 -1
  168. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +9 -11
  169. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +177 -134
  170. infrahub_testcontainers/container.py +1 -1
  171. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  172. infrahub_testcontainers/docker-compose.test.yml +1 -1
  173. infrahub_testcontainers/models.py +2 -2
  174. infrahub_testcontainers/performance_test.py +4 -4
  175. infrahub/core/convert_object_type/conversion.py +0 -134
  176. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
  177. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
  178. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
@@ -42,10 +42,12 @@ from infrahub.types import ATTRIBUTE_TYPES
42
42
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
43
43
  from ...graphql.models import OrderModel
44
44
  from ...log import get_logger
45
+ from ..attribute import BaseAttribute
45
46
  from ..query.relationship import RelationshipDeleteAllQuery
46
47
  from ..relationship import RelationshipManager
47
48
  from ..utils import update_relationships_to
48
49
  from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
50
+ from .node_property_attribute import DisplayLabel, HumanFriendlyIdentifier
49
51
 
50
52
  if TYPE_CHECKING:
51
53
  from typing_extensions import Self
@@ -53,8 +55,6 @@ if TYPE_CHECKING:
53
55
  from infrahub.core.branch import Branch
54
56
  from infrahub.database import InfrahubDatabase
55
57
 
56
- from ..attribute import BaseAttribute
57
-
58
58
  SchemaProtocol = TypeVar("SchemaProtocol")
59
59
 
60
60
  # ---------------------------------------------------------------------------------------
@@ -80,6 +80,29 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
80
80
  _meta.default_filter = default_filter
81
81
  super().__init_subclass_with_meta__(_meta=_meta, **options)
82
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
+
83
106
  def get_schema(self) -> NonGenericSchemaTypes:
84
107
  return self._schema
85
108
 
@@ -100,16 +123,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
100
123
  def get_updated_at(self) -> Timestamp | None:
101
124
  return self._updated_at
102
125
 
126
+ def get_attribute(self, name: str) -> BaseAttribute:
127
+ attribute = getattr(self, name)
128
+ if not isinstance(attribute, BaseAttribute):
129
+ raise ValueError(f"{name} is not an attribute of {self.get_kind()}")
130
+ return attribute
131
+
132
+ def get_relationship(self, name: str) -> RelationshipManager:
133
+ relationship = getattr(self, name)
134
+ if not isinstance(relationship, RelationshipManager):
135
+ raise ValueError(f"{name} is not a relationship of {self.get_kind()}")
136
+ return relationship
137
+
138
+ def uses_profiles(self) -> bool:
139
+ for attr_name in self.get_schema().attribute_names:
140
+ try:
141
+ node_attr = self.get_attribute(attr_name)
142
+ except ValueError:
143
+ continue
144
+ if node_attr and node_attr.is_from_profile:
145
+ return True
146
+ return False
147
+
103
148
  async def get_hfid(self, db: InfrahubDatabase, include_kind: bool = False) -> list[str] | None:
104
149
  """Return the Human friendly id of the node."""
105
150
  if not self._schema.human_friendly_id:
106
151
  return None
107
152
 
108
- 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
+
109
159
  hfid = [value for value in hfid_values if value is not None]
110
- if include_kind:
111
- return [self.get_kind()] + hfid
112
- return hfid
160
+ return [self.get_kind()] + hfid if include_kind else hfid
113
161
 
114
162
  async def get_hfid_as_string(self, db: InfrahubDatabase, include_kind: bool = False) -> str | None:
115
163
  """Return the Human friendly id of the node in string format separated with a dunder (__) ."""
@@ -118,6 +166,37 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
118
166
  return None
119
167
  return "__".join(hfid)
120
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
+
121
200
  async def get_path_value(self, db: InfrahubDatabase, path: str) -> str:
122
201
  schema_path = self._schema.parse_schema_path(
123
202
  path=path, schema=db.schema.get_schema_branch(name=self._branch.name)
@@ -176,30 +255,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
176
255
  return self._branch
177
256
 
178
257
  def __repr__(self) -> str:
179
- if not self._existing:
180
- return f"{self.get_kind()}(ID: {str(self.id)})[NEW]"
181
-
182
- return f"{self.get_kind()}(ID: {str(self.id)})"
183
-
184
- def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
185
- self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
186
- self._branch: Branch = branch
187
- self._at: Timestamp = at
188
- self._existing: bool = False
189
-
190
- self._updated_at: Timestamp | None = None
191
- self.id: str = None
192
- self.db_id: str = None
193
-
194
- self._source: Node | None = None
195
- self._owner: Node | None = None
196
- self._is_protected: bool = None
197
- self._computed_jinja2_attributes: list[str] = []
198
-
199
- # Lists of attributes and relationships names
200
- self._attributes: list[str] = []
201
- self._relationships: list[str] = []
202
- 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]"
203
260
 
204
261
  @property
205
262
  def node_changelog(self) -> NodeChangelog:
@@ -712,12 +769,26 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
712
769
  )
713
770
  self._updated_at = Timestamp(updated_at)
714
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
+
715
782
  await self._process_fields(db=db, fields=kwargs)
716
783
  return self
717
784
 
718
785
  async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> NodeChangelog:
719
786
  create_at = Timestamp(at)
720
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
+
721
792
  query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at)
722
793
  await query.execute(db=db)
723
794
 
@@ -729,6 +800,13 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
729
800
  new_ids = query.get_ids()
730
801
  node_changelog = NodeChangelog(node_id=self.get_id(), node_kind=self.get_kind(), display_label="")
731
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
+
732
810
  # Go over the list of Attribute and assign the new IDs one by one
733
811
  for name in self._attributes:
734
812
  attr: BaseAttribute = getattr(self, name)
@@ -741,12 +819,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
741
819
  relm: RelationshipManager = getattr(self, name)
742
820
  for rel in relm._relationships:
743
821
  identifier = f"{rel.schema.identifier}::{rel.peer_id}"
744
-
745
822
  rel.id, rel.db_id = new_ids[identifier]
746
-
747
823
  node_changelog.create_relationship(relationship=rel)
748
824
 
749
- node_changelog.display_label = await self.render_display_label(db=db)
825
+ node_changelog.display_label = await self.get_display_label(db=db)
750
826
  return node_changelog
751
827
 
752
828
  async def _update(
@@ -782,19 +858,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
782
858
  if parent := await rel.get_parent(db=db):
783
859
  node_changelog.add_parent_from_relationship(parent=parent)
784
860
 
785
- 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)
786
885
  return node_changelog
787
886
 
788
887
  async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> Self:
789
888
  """Create or Update the Node in the database."""
790
-
791
889
  save_at = Timestamp(at)
792
890
 
793
891
  if self._existing:
794
892
  self._node_changelog = await self._update(at=save_at, db=db, fields=fields)
795
- return self
893
+ else:
894
+ self._node_changelog = await self._create(at=save_at, db=db)
796
895
 
797
- self._node_changelog = await self._create(at=save_at, db=db)
798
896
  return self
799
897
 
800
898
  async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
@@ -803,13 +901,24 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
803
901
  delete_at = Timestamp(at)
804
902
 
805
903
  node_changelog = NodeChangelog(
806
- 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)
807
905
  )
808
906
  # Go over the list of Attribute and update them one by one
809
907
  for name in self._attributes:
810
908
  attr: BaseAttribute = getattr(self, name)
811
- deleted_attribute = await attr.delete(at=delete_at, db=db)
812
- 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
+ ):
813
922
  node_changelog.add_attribute(attribute=deleted_attribute)
814
923
 
815
924
  branch = self.get_branch_based_on_support_type()
@@ -877,7 +986,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
877
986
  continue
878
987
 
879
988
  if field_name == "display_label":
880
- response[field_name] = await self.render_display_label(db=db)
989
+ response[field_name] = await self.get_display_label(db=db)
881
990
  continue
882
991
 
883
992
  if field_name == "hfid":
@@ -980,6 +1089,20 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
980
1089
  return repr(self)
981
1090
  return display_label.strip()
982
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
+
983
1106
  def _get_parent_relationship_name(self) -> str | None:
984
1107
  """Return the name of the parent relationship is one is present"""
985
1108
  for relationship in self._schema.relationships:
@@ -989,7 +1112,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
989
1112
  return None
990
1113
 
991
1114
  async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
992
- object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
1115
+ object_template: RelationshipManager | None = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
993
1116
  return (
994
1117
  await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
995
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
@@ -2,18 +2,23 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Mapping
4
4
 
5
+ from infrahub import lock
5
6
  from infrahub.core import registry
6
7
  from infrahub.core.constants import RelationshipCardinality, RelationshipKind
7
8
  from infrahub.core.constraint.node.runner import NodeConstraintRunner
8
- from infrahub.core.manager import NodeManager
9
9
  from infrahub.core.node import Node
10
+ from infrahub.core.node.lock_utils import get_kind_lock_names_on_object_mutation
10
11
  from infrahub.core.protocols import CoreObjectTemplate
12
+ from infrahub.core.schema import GenericSchema
11
13
  from infrahub.dependencies.registry import get_component_registry
14
+ from infrahub.lock import InfrahubMultiLock
15
+ from infrahub.profiles.node_applier import NodeProfilesApplier
12
16
 
13
17
  if TYPE_CHECKING:
14
18
  from infrahub.core.branch import Branch
15
19
  from infrahub.core.relationship.model import RelationshipManager
16
20
  from infrahub.core.schema import MainSchemaTypes, NonGenericSchemaTypes, RelationshipSchema
21
+ from infrahub.core.timestamp import Timestamp
17
22
  from infrahub.database import InfrahubDatabase
18
23
 
19
24
 
@@ -76,6 +81,7 @@ async def handle_template_relationships(
76
81
  template: CoreObjectTemplate,
77
82
  fields: list,
78
83
  constraint_runner: NodeConstraintRunner | None = None,
84
+ at: Timestamp | None = None,
79
85
  ) -> None:
80
86
  if constraint_runner is None:
81
87
  component_registry = get_component_registry()
@@ -103,7 +109,7 @@ async def handle_template_relationships(
103
109
  current_template=template,
104
110
  )
105
111
 
106
- obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch)
112
+ obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch, at=at)
107
113
  await obj_peer.new(db=db, **obj_peer_data)
108
114
  await constraint_runner.check(node=obj_peer, field_filters=list(obj_peer_data))
109
115
  await obj_peer.save(db=db)
@@ -115,6 +121,7 @@ async def handle_template_relationships(
115
121
  obj=obj_peer,
116
122
  template=template_relationship_peer,
117
123
  fields=fields,
124
+ at=at,
118
125
  )
119
126
 
120
127
 
@@ -125,43 +132,20 @@ async def get_profile_ids(db: InfrahubDatabase, obj: Node) -> set[str]:
125
132
  return {pr.peer_id for pr in profile_rels}
126
133
 
127
134
 
128
- async def refresh_for_profile_update(
129
- db: InfrahubDatabase,
130
- branch: Branch,
131
- obj: Node,
132
- schema: NonGenericSchemaTypes,
133
- previous_profile_ids: set[str] | None = None,
134
- ) -> Node:
135
- if not hasattr(obj, "profiles"):
136
- return obj
137
- current_profile_ids = await get_profile_ids(db=db, obj=obj)
138
- if previous_profile_ids is None or previous_profile_ids != current_profile_ids:
139
- refreshed_node = await NodeManager.get_one_by_id_or_default_filter(
140
- db=db,
141
- kind=schema.kind,
142
- id=obj.get_id(),
143
- branch=branch,
144
- include_owner=True,
145
- include_source=True,
146
- )
147
- refreshed_node._node_changelog = obj.node_changelog
148
- return refreshed_node
149
- return obj
150
-
151
-
152
135
  async def _do_create_node(
153
136
  node_class: type[Node],
137
+ node_constraint_runner: NodeConstraintRunner,
154
138
  db: InfrahubDatabase,
155
- data: dict,
156
139
  schema: NonGenericSchemaTypes,
157
- fields_to_validate: list,
158
140
  branch: Branch,
159
- node_constraint_runner: NodeConstraintRunner,
141
+ fields_to_validate: list[str],
142
+ data: dict[str, Any],
143
+ at: Timestamp | None = None,
160
144
  ) -> Node:
161
145
  obj = await node_class.init(db=db, schema=schema, branch=branch)
162
146
  await obj.new(db=db, **data)
163
147
  await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
164
- await obj.save(db=db)
148
+ await obj.save(db=db, at=at)
165
149
 
166
150
  object_template = await obj.get_object_template(db=db)
167
151
  if object_template:
@@ -171,18 +155,62 @@ async def _do_create_node(
171
155
  template=object_template,
172
156
  obj=obj,
173
157
  fields=fields_to_validate,
158
+ at=at,
174
159
  )
175
160
  return obj
176
161
 
177
162
 
163
+ async def _do_create_node_with_lock(
164
+ node_class: type[Node],
165
+ node_constraint_runner: NodeConstraintRunner,
166
+ db: InfrahubDatabase,
167
+ schema: NonGenericSchemaTypes,
168
+ branch: Branch,
169
+ fields_to_validate: list[str],
170
+ data: dict[str, Any],
171
+ at: Timestamp | None = None,
172
+ ) -> Node:
173
+ schema_branch = registry.schema.get_schema_branch(name=branch.name)
174
+ lock_names = get_kind_lock_names_on_object_mutation(
175
+ kind=schema.kind, branch=branch, schema_branch=schema_branch, data=dict(data)
176
+ )
177
+
178
+ if lock_names:
179
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
180
+ return await _do_create_node(
181
+ node_class=node_class,
182
+ node_constraint_runner=node_constraint_runner,
183
+ db=db,
184
+ schema=schema,
185
+ branch=branch,
186
+ fields_to_validate=fields_to_validate,
187
+ data=data,
188
+ at=at,
189
+ )
190
+ return await _do_create_node(
191
+ node_class=node_class,
192
+ node_constraint_runner=node_constraint_runner,
193
+ db=db,
194
+ schema=schema,
195
+ branch=branch,
196
+ fields_to_validate=fields_to_validate,
197
+ data=data,
198
+ at=at,
199
+ )
200
+
201
+
178
202
  async def create_node(
179
- data: dict,
203
+ data: dict[str, Any],
180
204
  db: InfrahubDatabase,
181
205
  branch: Branch,
182
- schema: NonGenericSchemaTypes,
206
+ schema: MainSchemaTypes,
207
+ at: Timestamp | None = None,
183
208
  ) -> Node:
184
209
  """Create a node in the database if constraint checks succeed."""
185
210
 
211
+ if isinstance(schema, GenericSchema):
212
+ raise ValueError(f"Node of generic schema `{schema.name=}` can not be instantiated.")
213
+
186
214
  component_registry = get_component_registry()
187
215
  node_constraint_runner = await component_registry.get_component(
188
216
  NodeConstraintRunner, db=db.start_session() if not db.is_transaction else db, branch=branch
@@ -193,7 +221,7 @@ async def create_node(
193
221
 
194
222
  fields_to_validate = list(data)
195
223
  if db.is_transaction:
196
- obj = await _do_create_node(
224
+ obj = await _do_create_node_with_lock(
197
225
  node_class=node_class,
198
226
  node_constraint_runner=node_constraint_runner,
199
227
  db=db,
@@ -201,10 +229,11 @@ async def create_node(
201
229
  branch=branch,
202
230
  fields_to_validate=fields_to_validate,
203
231
  data=data,
232
+ at=at,
204
233
  )
205
234
  else:
206
235
  async with db.start_transaction() as dbt:
207
- obj = await _do_create_node(
236
+ obj = await _do_create_node_with_lock(
208
237
  node_class=node_class,
209
238
  node_constraint_runner=node_constraint_runner,
210
239
  db=dbt,
@@ -212,9 +241,12 @@ async def create_node(
212
241
  branch=branch,
213
242
  fields_to_validate=fields_to_validate,
214
243
  data=data,
244
+ at=at,
215
245
  )
216
246
 
217
247
  if await get_profile_ids(db=db, obj=obj):
218
- obj = await refresh_for_profile_update(db=db, branch=branch, schema=schema, obj=obj)
248
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
249
+ await node_profiles_applier.apply_profiles(node=obj)
250
+ await obj.save(db=db)
219
251
 
220
252
  return obj
@@ -0,0 +1,98 @@
1
+ import hashlib
2
+ from typing import Any
3
+
4
+ from infrahub.core.branch import Branch
5
+ from infrahub.core.constants.infrahubkind import GENERICGROUP, GRAPHQLQUERYGROUP
6
+ from infrahub.core.schema import GenericSchema
7
+ from infrahub.core.schema.schema_branch import SchemaBranch
8
+
9
+ KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED = [GENERICGROUP]
10
+
11
+
12
+ def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch) -> list[str]:
13
+ """
14
+ Return kinds for which we want to lock during creating / updating an object of a given schema node.
15
+ Lock should be performed on schema kind and its generics having a uniqueness_constraint defined.
16
+ If a generic uniqueness constraint is the same as the node schema one,
17
+ it means node schema overrided this constraint, in which case we only need to lock on the generic.
18
+ """
19
+
20
+ node_schema = schema_branch.get(name=kind, duplicate=False)
21
+
22
+ schema_uc = None
23
+ kinds = []
24
+ if node_schema.uniqueness_constraints:
25
+ kinds.append(node_schema.kind)
26
+ schema_uc = node_schema.uniqueness_constraints
27
+
28
+ if isinstance(node_schema, GenericSchema):
29
+ return kinds
30
+
31
+ generics_kinds = node_schema.inherit_from
32
+
33
+ node_schema_kind_removed = False
34
+ for generic_kind in generics_kinds:
35
+ generic_uc = schema_branch.get(name=generic_kind, duplicate=False).uniqueness_constraints
36
+ if generic_uc:
37
+ kinds.append(generic_kind)
38
+ if not node_schema_kind_removed and generic_uc == schema_uc:
39
+ # Check whether we should remove original schema kind as it simply overrides uniqueness_constraint
40
+ # of a generic
41
+ kinds.pop(0)
42
+ node_schema_kind_removed = True
43
+ return kinds
44
+
45
+
46
+ def _should_kind_be_locked_on_any_branch(kind: str, schema_branch: SchemaBranch) -> bool:
47
+ """
48
+ Check whether kind or any kind generic is in KINDS_TO_LOCK_ON_ANY_BRANCH.
49
+ """
50
+
51
+ if kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
52
+ return True
53
+
54
+ node_schema = schema_branch.get(name=kind, duplicate=False)
55
+ if isinstance(node_schema, GenericSchema):
56
+ return False
57
+
58
+ for generic_kind in node_schema.inherit_from:
59
+ if generic_kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
60
+ return True
61
+ return False
62
+
63
+
64
+ def _hash(value: str) -> str:
65
+ # Do not use builtin `hash` for lock names as due to randomization results would differ between
66
+ # different processes.
67
+ return hashlib.sha256(value.encode()).hexdigest()
68
+
69
+
70
+ def get_kind_lock_names_on_object_mutation(
71
+ kind: str, branch: Branch, schema_branch: SchemaBranch, data: dict[str, Any]
72
+ ) -> list[str]:
73
+ """
74
+ Return objects kind for which we want to avoid concurrent mutation (create/update). Except for some specific kinds,
75
+ concurrent mutations are only allowed on non-main branch as objects validations will be performed at least when merging in main branch.
76
+ """
77
+
78
+ if not branch.is_default and not _should_kind_be_locked_on_any_branch(kind=kind, schema_branch=schema_branch):
79
+ return []
80
+
81
+ if kind == GRAPHQLQUERYGROUP:
82
+ # Lock on name as well to improve performances
83
+ try:
84
+ name = data["name"].value
85
+ return [build_object_lock_name(kind + "." + _hash(name))]
86
+ except KeyError:
87
+ # We might reach here if we are updating a CoreGraphQLQueryGroup without updating the name,
88
+ # in which case we would not need to lock. This is not supposed to happen as current `update`
89
+ # logic first fetches the node with its name.
90
+ return []
91
+
92
+ lock_kinds = _get_kinds_to_lock_on_object_mutation(kind, schema_branch)
93
+ lock_names = [build_object_lock_name(kind) for kind in lock_kinds]
94
+ return lock_names
95
+
96
+
97
+ def build_object_lock_name(name: str) -> str:
98
+ return f"global.object.{name}"