infrahub-server 1.3.0b5__py3-none-any.whl → 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. infrahub/actions/constants.py +36 -79
  2. infrahub/actions/schema.py +2 -0
  3. infrahub/cli/db.py +7 -5
  4. infrahub/cli/upgrade.py +6 -1
  5. infrahub/core/attribute.py +5 -0
  6. infrahub/core/constraint/node/runner.py +3 -1
  7. infrahub/core/convert_object_type/conversion.py +2 -0
  8. infrahub/core/diff/coordinator.py +8 -1
  9. infrahub/core/diff/query/delete_query.py +8 -4
  10. infrahub/core/diff/query/field_specifiers.py +1 -1
  11. infrahub/core/diff/query/merge.py +2 -2
  12. infrahub/core/diff/repository/repository.py +4 -0
  13. infrahub/core/graph/__init__.py +1 -1
  14. infrahub/core/migrations/graph/__init__.py +2 -0
  15. infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
  16. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  17. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  18. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
  19. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  20. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +2 -2
  21. infrahub/core/migrations/graph/m031_check_number_attributes.py +102 -0
  22. infrahub/core/migrations/query/attribute_rename.py +1 -1
  23. infrahub/core/node/__init__.py +70 -37
  24. infrahub/core/path.py +14 -0
  25. infrahub/core/query/delete.py +3 -3
  26. infrahub/core/relationship/constraints/count.py +10 -9
  27. infrahub/core/relationship/constraints/interface.py +2 -1
  28. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  29. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  30. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  31. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  32. infrahub/core/schema/attribute_parameters.py +12 -5
  33. infrahub/core/schema/basenode_schema.py +107 -1
  34. infrahub/core/schema/definitions/internal.py +8 -1
  35. infrahub/core/schema/generated/relationship_schema.py +6 -1
  36. infrahub/core/schema/schema_branch.py +53 -13
  37. infrahub/core/validators/__init__.py +2 -1
  38. infrahub/core/validators/attribute/min_max.py +7 -2
  39. infrahub/core/validators/relationship/peer.py +174 -4
  40. infrahub/database/__init__.py +0 -1
  41. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  42. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  43. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  44. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  45. infrahub/dependencies/registry.py +2 -0
  46. infrahub/git/tasks.py +1 -0
  47. infrahub/graphql/app.py +5 -1
  48. infrahub/graphql/mutations/convert_object_type.py +16 -7
  49. infrahub/graphql/mutations/relationship.py +32 -0
  50. infrahub/graphql/queries/convert_object_type_mapping.py +3 -5
  51. infrahub/message_bus/operations/refresh/registry.py +3 -6
  52. infrahub/pools/models.py +14 -0
  53. infrahub/pools/tasks.py +71 -1
  54. infrahub/services/adapters/message_bus/nats.py +5 -1
  55. infrahub/services/scheduler.py +5 -1
  56. infrahub_sdk/ctl/generator.py +4 -4
  57. infrahub_sdk/ctl/repository.py +1 -1
  58. infrahub_sdk/node/__init__.py +2 -0
  59. infrahub_sdk/node/node.py +166 -93
  60. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  61. infrahub_sdk/query_groups.py +4 -3
  62. infrahub_sdk/utils.py +7 -20
  63. infrahub_sdk/yaml.py +6 -5
  64. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/METADATA +2 -2
  65. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/RECORD +68 -63
  66. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/LICENSE.txt +0 -0
  67. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/WHEEL +0 -0
  68. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/entry_points.txt +0 -0
@@ -710,7 +710,9 @@ class SchemaBranch:
710
710
  ):
711
711
  unique_attrs_in_constraints.add(schema_attribute_path.attribute_schema.name)
712
712
 
713
- unique_attrs_in_attrs = {attr_schema.name for attr_schema in node_schema.unique_attributes}
713
+ unique_attrs_in_attrs = {
714
+ attr_schema.name for attr_schema in node_schema.unique_attributes if not attr_schema.inherited
715
+ }
714
716
  if unique_attrs_in_attrs == unique_attrs_in_constraints:
715
717
  continue
716
718
 
@@ -822,11 +824,16 @@ class SchemaBranch:
822
824
  ) from exc
823
825
 
824
826
  def _is_attr_combination_unique(
825
- self, attrs_paths: list[str], uniqueness_constraints: list[list[str]] | None
827
+ self, attrs_paths: list[str], uniqueness_constraints: list[list[str]] | None, unique_attribute_names: list[str]
826
828
  ) -> bool:
827
829
  """
828
- Return whether at least one combination of any length of `attrs_paths` is equal to a uniqueness constraint.
830
+ Return whether at least one combination of any length of `attrs_paths` is unique
829
831
  """
832
+ if unique_attribute_names:
833
+ for attr_path in attrs_paths:
834
+ for unique_attr_name in unique_attribute_names:
835
+ if attr_path.startswith(unique_attr_name):
836
+ return True
830
837
 
831
838
  if not uniqueness_constraints:
832
839
  return False
@@ -868,9 +875,14 @@ class SchemaBranch:
868
875
  if config.SETTINGS.main.schema_strict_mode:
869
876
  # For every relationship referred within hfid, check whether the combination of attributes is unique is the peer schema node
870
877
  for related_schema, attrs_paths in rel_schemas_to_paths.values():
871
- if not self._is_attr_combination_unique(attrs_paths, related_schema.uniqueness_constraints):
878
+ if not self._is_attr_combination_unique(
879
+ attrs_paths=attrs_paths,
880
+ uniqueness_constraints=related_schema.uniqueness_constraints,
881
+ unique_attribute_names=[a.name for a in related_schema.unique_attributes],
882
+ ):
872
883
  raise ValidationError(
873
- f"HFID of {node_schema.kind} refers peer {related_schema.kind} with a non-unique combination of attributes {attrs_paths}"
884
+ f"HFID of {node_schema.kind} refers to peer {related_schema.kind}"
885
+ f" with a non-unique combination of attributes {attrs_paths}"
874
886
  )
875
887
 
876
888
  def validate_required_relationships(self) -> None:
@@ -975,6 +987,28 @@ class SchemaBranch:
975
987
  ):
976
988
  raise ValueError(f"{node.kind}: {rel.name} isn't allowed as a relationship name.")
977
989
 
990
+ def _validate_common_parent(self, node: NodeSchema, rel: RelationshipSchema) -> None:
991
+ if not rel.common_parent:
992
+ return
993
+
994
+ peer_schema = self.get(name=rel.peer, duplicate=False)
995
+ if not node.has_parent_relationship:
996
+ raise ValueError(
997
+ f"{node.kind}: Relationship {rel.name!r} defines 'common_parent' but node does not have a parent relationship"
998
+ )
999
+
1000
+ try:
1001
+ parent_rel = peer_schema.get_relationship(name=rel.common_parent)
1002
+ except ValueError as exc:
1003
+ raise ValueError(
1004
+ f"{node.kind}: Relationship {rel.name!r} defines 'common_parent' but '{rel.peer}.{rel.common_parent}' does not exist"
1005
+ ) from exc
1006
+
1007
+ if parent_rel.kind != RelationshipKind.PARENT:
1008
+ raise ValueError(
1009
+ f"{node.kind}: Relationship {rel.name!r} defines 'common_parent' but '{rel.peer}.{rel.common_parent} is not of kind 'parent'"
1010
+ )
1011
+
978
1012
  def validate_kinds(self) -> None:
979
1013
  for name in list(self.nodes.keys()):
980
1014
  node = self.get_node(name=name, duplicate=False)
@@ -997,6 +1031,9 @@ class SchemaBranch:
997
1031
  raise ValueError(
998
1032
  f"{node.kind}: Relationship {rel.name!r} is referring an invalid peer {rel.peer!r}"
999
1033
  ) from None
1034
+
1035
+ self._validate_common_parent(node=node, rel=rel)
1036
+
1000
1037
  if rel.common_relatives:
1001
1038
  peer_schema = self.get(name=rel.peer, duplicate=False)
1002
1039
  for common_relatives_rel_name in rel.common_relatives:
@@ -1019,11 +1056,7 @@ class SchemaBranch:
1019
1056
  for name in self.nodes.keys():
1020
1057
  node_schema = self.get_node(name=name, duplicate=False)
1021
1058
  for attribute in node_schema.attributes:
1022
- if (
1023
- attribute.kind == "NumberPool"
1024
- and isinstance(attribute.parameters, NumberPoolParameters)
1025
- and not attribute.parameters.number_pool_id
1026
- ):
1059
+ if attribute.kind == "NumberPool" and isinstance(attribute.parameters, NumberPoolParameters):
1027
1060
  self._validate_number_pool_parameters(
1028
1061
  node_schema=node_schema, attribute=attribute, number_pool_parameters=attribute.parameters
1029
1062
  )
@@ -1039,7 +1072,7 @@ class SchemaBranch:
1039
1072
  f"{node_schema.kind}.{attribute.name} is a NumberPool it has to be a read_only attribute"
1040
1073
  )
1041
1074
 
1042
- if attribute.inherited:
1075
+ if attribute.inherited and not number_pool_parameters.number_pool_id:
1043
1076
  generics_with_attribute = []
1044
1077
  for generic_name in node_schema.inherit_from:
1045
1078
  generic_schema = self.get_generic(name=generic_name, duplicate=False)
@@ -1053,9 +1086,16 @@ class SchemaBranch:
1053
1086
  raise ValidationError(
1054
1087
  f"{node_schema.kind}.{attribute.name} is a NumberPool inherited from more than one generic"
1055
1088
  )
1089
+ elif not attribute.inherited:
1090
+ for generic_name in node_schema.inherit_from:
1091
+ generic_schema = self.get_generic(name=generic_name, duplicate=False)
1092
+ if attribute.name in generic_schema.attribute_names:
1093
+ raise ValidationError(
1094
+ f"Overriding '{node_schema.kind}.{attribute.name}' NumberPool attribute from generic '{generic_name}' is not supported"
1095
+ )
1056
1096
 
1057
- else:
1058
- number_pool_parameters.number_pool_id = str(uuid4())
1097
+ if not number_pool_parameters.number_pool_id:
1098
+ number_pool_parameters.number_pool_id = str(uuid4())
1059
1099
 
1060
1100
  def validate_computed_attributes(self) -> None:
1061
1101
  self.computed_attributes = ComputedAttributes()
@@ -17,7 +17,7 @@ from .node.inherit_from import NodeInheritFromChecker
17
17
  from .node.relationship import NodeRelationshipAddChecker
18
18
  from .relationship.count import RelationshipCountChecker
19
19
  from .relationship.optional import RelationshipOptionalChecker
20
- from .relationship.peer import RelationshipPeerChecker
20
+ from .relationship.peer import RelationshipPeerChecker, RelationshipPeerParentChecker
21
21
  from .uniqueness.checker import UniquenessChecker
22
22
 
23
23
  CONSTRAINT_VALIDATOR_MAP: dict[str, type[ConstraintCheckerInterface] | None] = {
@@ -42,6 +42,7 @@ CONSTRAINT_VALIDATOR_MAP: dict[str, type[ConstraintCheckerInterface] | None] = {
42
42
  "relationship.optional.update": RelationshipOptionalChecker,
43
43
  "relationship.min_count.update": RelationshipCountChecker,
44
44
  "relationship.max_count.update": RelationshipCountChecker,
45
+ "relationship.common_parent.update": RelationshipPeerParentChecker,
45
46
  "node.inherit_from.update": NodeInheritFromChecker,
46
47
  "node.uniqueness_constraints.update": UniquenessChecker,
47
48
  "node.parent.update": NodeHierarchyChecker,
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
+ from infrahub import config
5
6
  from infrahub.core.constants import PathType
6
7
  from infrahub.core.path import DataPath, GroupedDataPaths
7
8
  from infrahub.core.schema.attribute_parameters import NumberAttributeParameters
@@ -87,6 +88,10 @@ class AttributeNumberChecker(ConstraintCheckerInterface):
87
88
  return "attribute.number.update"
88
89
 
89
90
  def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
91
+ # Some invalid values may exist due to https://github.com/opsmill/infrahub/issues/6714.
92
+ if not config.SETTINGS.main.schema_strict_mode:
93
+ return False
94
+
90
95
  return request.constraint_name in (
91
96
  ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MIN_VALUE_UPDATE.value,
92
97
  ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MAX_VALUE_UPDATE.value,
@@ -94,7 +99,6 @@ class AttributeNumberChecker(ConstraintCheckerInterface):
94
99
  )
95
100
 
96
101
  async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
97
- grouped_data_paths_list: list[GroupedDataPaths] = []
98
102
  if not request.schema_path.field_name:
99
103
  raise ValueError("field_name is not defined")
100
104
  attribute_schema = request.node_schema.get_attribute(name=request.schema_path.field_name)
@@ -106,8 +110,9 @@ class AttributeNumberChecker(ConstraintCheckerInterface):
106
110
  and attribute_schema.parameters.max_value is None
107
111
  and attribute_schema.parameters.excluded_values is None
108
112
  ):
109
- return grouped_data_paths_list
113
+ return []
110
114
 
115
+ grouped_data_paths_list: list[GroupedDataPaths] = []
111
116
  for query_class in self.query_classes:
112
117
  # TODO add exception handling
113
118
  query = await query_class.init(
@@ -2,17 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
- from infrahub.core.constants import PathType
5
+ from infrahub import config
6
+ from infrahub.core.constants import PathType, RelationshipKind
6
7
  from infrahub.core.path import DataPath, GroupedDataPaths
7
8
  from infrahub.core.schema import GenericSchema
8
9
 
9
10
  from ..interface import ConstraintCheckerInterface
10
- from ..shared import (
11
- RelationshipSchemaValidatorQuery,
12
- )
11
+ from ..shared import RelationshipSchemaValidatorQuery
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from infrahub.core.branch import Branch
15
+ from infrahub.core.schema.relationship_schema import RelationshipSchema
16
16
  from infrahub.database import InfrahubDatabase
17
17
 
18
18
  from ..model import SchemaConstraintValidatorRequest
@@ -125,3 +125,173 @@ class RelationshipPeerChecker(ConstraintCheckerInterface):
125
125
  await query.execute(db=self.db)
126
126
  grouped_data_paths_list.append(await query.get_paths())
127
127
  return grouped_data_paths_list
128
+
129
+
130
+ class RelationshipPeerParentValidatorQuery(RelationshipSchemaValidatorQuery):
131
+ name = "relationship_constraints_peer_parent_validator"
132
+
133
+ def __init__(
134
+ self,
135
+ relationship: RelationshipSchema,
136
+ parent_relationship: RelationshipSchema,
137
+ peer_parent_relationship: RelationshipSchema,
138
+ **kwargs: Any,
139
+ ):
140
+ super().__init__(**kwargs)
141
+
142
+ self.relationship = relationship
143
+ self.parent_relationship = parent_relationship
144
+ self.peer_parent_relationship = peer_parent_relationship
145
+
146
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
147
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string(), is_isolated=False)
148
+ self.params.update(branch_params)
149
+ self.params["peer_relationship_id"] = self.relationship.identifier
150
+ self.params["parent_relationship_id"] = self.parent_relationship.identifier
151
+ self.params["peer_parent_relationship_id"] = self.peer_parent_relationship.identifier
152
+
153
+ parent_arrows = self.parent_relationship.get_query_arrows()
154
+ parent_match = (
155
+ "MATCH (active_node)%(lstart)s[r1:IS_RELATED]%(lend)s"
156
+ "(rel:Relationship { name: $parent_relationship_id })%(rstart)s[r2:IS_RELATED]%(rend)s(parent:Node)"
157
+ ) % {
158
+ "lstart": parent_arrows.left.start,
159
+ "lend": parent_arrows.left.end,
160
+ "rstart": parent_arrows.right.start,
161
+ "rend": parent_arrows.right.end,
162
+ }
163
+
164
+ peer_parent_arrows = self.relationship.get_query_arrows()
165
+ peer_match = (
166
+ "MATCH (active_node)%(lstart)s[r1:IS_RELATED]%(lend)s"
167
+ "(r:Relationship {name: $peer_relationship_id })%(rstart)s[r2:IS_RELATED]%(rend)s(peer:Node)"
168
+ ) % {
169
+ "lstart": peer_parent_arrows.left.start,
170
+ "lend": peer_parent_arrows.left.end,
171
+ "rstart": peer_parent_arrows.right.start,
172
+ "rend": peer_parent_arrows.right.end,
173
+ }
174
+
175
+ peer_parent_arrows = self.peer_parent_relationship.get_query_arrows()
176
+ peer_parent_match = (
177
+ "MATCH (peer:Node)%(lstart)s[r1:IS_RELATED]%(lend)s"
178
+ "(r:Relationship {name: $peer_parent_relationship_id})%(rstart)s[r2:IS_RELATED]%(rend)s(peer_parent:Node)"
179
+ ) % {
180
+ "lstart": peer_parent_arrows.left.start,
181
+ "lend": peer_parent_arrows.left.end,
182
+ "rstart": peer_parent_arrows.right.start,
183
+ "rend": peer_parent_arrows.right.end,
184
+ }
185
+
186
+ query = """
187
+ MATCH (n:%(node_kind)s)
188
+ CALL (n) {
189
+ MATCH path = (root:Root)<-[r:IS_PART_OF]-(n)
190
+ WHERE %(branch_filter)s
191
+ RETURN n as active_node, r.status = "active" AS is_active
192
+ ORDER BY r.branch_level DESC, r.from DESC
193
+ LIMIT 1
194
+ }
195
+ WITH active_node, is_active
196
+ WHERE is_active = TRUE
197
+ %(parent_match)s
198
+ WHERE all(r in [r1, r2] WHERE %(branch_filter)s AND r.status = "active")
199
+ CALL (active_node) {
200
+ %(peer_match)s
201
+ WITH DISTINCT active_node, peer
202
+ %(peer_match)s
203
+ WHERE all(r in [r1, r2] WHERE %(branch_filter)s)
204
+ WITH peer, r1.status = "active" AND r2.status = "active" AS is_active
205
+ ORDER BY peer.uuid, r1.branch_level DESC, r2.branch_level DESC, r1.from DESC, r2.from DESC, is_active DESC
206
+ WITH peer, head(collect(is_active)) AS is_active
207
+ WHERE is_active = TRUE
208
+ RETURN peer
209
+ }
210
+ CALL (peer) {
211
+ %(peer_parent_match)s
212
+ WHERE all(r IN [r1, r2] WHERE %(branch_filter)s)
213
+ WITH peer_parent, r1, r2, r1.status = "active" AND r2.status = "active" AS is_active
214
+ WITH peer_parent, r1.branch AS branch_name, is_active
215
+ ORDER BY r1.branch_level DESC, r2.branch_level DESC, r1.from DESC, r2.from DESC, is_active DESC
216
+ LIMIT 1
217
+ WITH peer_parent, branch_name
218
+ WHERE is_active = TRUE
219
+ RETURN peer_parent, branch_name
220
+ }
221
+ WITH DISTINCT active_node, parent, peer, peer_parent, branch_name
222
+ WHERE parent.uuid <> peer_parent.uuid
223
+ """ % {
224
+ "branch_filter": branch_filter,
225
+ "node_kind": self.node_schema.kind,
226
+ "parent_match": parent_match,
227
+ "peer_match": peer_match,
228
+ "peer_parent_match": peer_parent_match,
229
+ }
230
+
231
+ self.add_to_query(query)
232
+ self.return_labels = ["active_node.uuid", "parent.uuid", "peer.uuid", "peer_parent.uuid", "branch_name"]
233
+
234
+ async def get_paths(self) -> GroupedDataPaths:
235
+ grouped_data_paths = GroupedDataPaths()
236
+
237
+ for result in self.results:
238
+ grouped_data_paths.add_data_path(
239
+ DataPath(
240
+ branch=str(result.get("branch_name")),
241
+ path_type=PathType.RELATIONSHIP_ONE,
242
+ node_id=str(result.get("peer.uuid")),
243
+ field_name=self.peer_parent_relationship.name,
244
+ peer_id=str(result.get("peer_parent.uuid")),
245
+ kind=self.relationship.peer,
246
+ )
247
+ )
248
+
249
+ return grouped_data_paths
250
+
251
+
252
+ class RelationshipPeerParentChecker(ConstraintCheckerInterface):
253
+ query_classes = [RelationshipPeerParentValidatorQuery]
254
+
255
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
256
+ self.db = db
257
+ self.branch = branch
258
+
259
+ @property
260
+ def name(self) -> str:
261
+ return "relationship.common_parent.update"
262
+
263
+ def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
264
+ return request.constraint_name == self.name and config.SETTINGS.main.schema_strict_mode
265
+
266
+ async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
267
+ grouped_data_paths_list: list[GroupedDataPaths] = []
268
+
269
+ if not request.schema_path.field_name:
270
+ return grouped_data_paths_list
271
+
272
+ relationship = request.node_schema.get_relationship(name=request.schema_path.field_name)
273
+ if not relationship.common_parent:
274
+ # Should not happen if schema validation was done properly
275
+ return grouped_data_paths_list
276
+
277
+ parent_relationship = next(
278
+ iter(request.node_schema.get_relationships_of_kind(relationship_kinds=[RelationshipKind.PARENT]))
279
+ )
280
+ peer_parent_relationship = request.schema_branch.get(name=relationship.peer, duplicate=False).get_relationship(
281
+ name=relationship.common_parent
282
+ )
283
+
284
+ for query_class in self.query_classes:
285
+ query = await query_class.init(
286
+ db=self.db,
287
+ branch=self.branch,
288
+ node_schema=request.node_schema,
289
+ schema_path=request.schema_path,
290
+ relationship=relationship,
291
+ parent_relationship=parent_relationship,
292
+ peer_parent_relationship=peer_parent_relationship,
293
+ )
294
+ await query.execute(db=self.db)
295
+ grouped_data_paths_list.append(await query.get_paths())
296
+
297
+ return grouped_data_paths_list
@@ -498,7 +498,6 @@ async def get_db(retry: int = 0) -> AsyncDriver:
498
498
  trusted_certificates=trusted_certificates,
499
499
  notifications_disabled_categories=[
500
500
  NotificationDisabledCategory.UNRECOGNIZED,
501
- NotificationDisabledCategory.DEPRECATION, # TODO: Remove me with 1.3
502
501
  ],
503
502
  notifications_min_severity=NotificationMinimumSeverity.WARNING,
504
503
  )
@@ -4,6 +4,7 @@ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilder
4
4
  from ..node.grouped_uniqueness import NodeGroupedUniquenessConstraintDependency
5
5
  from ..relationship_manager.count import RelationshipCountConstraintDependency
6
6
  from ..relationship_manager.peer_kind import RelationshipPeerKindConstraintDependency
7
+ from ..relationship_manager.peer_parent import RelationshipPeerParentConstraintDependency
7
8
  from ..relationship_manager.peer_relatives import RelationshipPeerRelativesConstraintDependency
8
9
  from ..relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
9
10
 
@@ -19,6 +20,7 @@ class NodeConstraintRunnerDependency(DependencyBuilder[NodeConstraintRunner]):
19
20
  RelationshipPeerKindConstraintDependency.build(context=context),
20
21
  RelationshipCountConstraintDependency.build(context=context),
21
22
  RelationshipProfilesKindConstraintDependency.build(context=context),
23
+ RelationshipPeerParentConstraintDependency.build(context=context),
22
24
  RelationshipPeerRelativesConstraintDependency.build(context=context),
23
25
  ],
24
26
  )
@@ -0,0 +1,8 @@
1
+ from infrahub.core.relationship.constraints.peer_parent import RelationshipPeerParentConstraint
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class RelationshipPeerParentConstraintDependency(DependencyBuilder[RelationshipPeerParentConstraint]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> RelationshipPeerParentConstraint:
8
+ return RelationshipPeerParentConstraint(db=context.db, branch=context.branch)
@@ -14,6 +14,7 @@ from .node_attribute import SchemaNodeAttributeAddConstraintDependency
14
14
  from .node_relationship import SchemaNodeRelationshipAddConstraintDependency
15
15
  from .relationship_count import SchemaRelationshipCountConstraintDependency
16
16
  from .relationship_optional import SchemaRelationshipOptionalConstraintDependency
17
+ from .relationship_peer import SchemaRelationshipPeerParentConstraintDependency
17
18
  from .uniqueness import SchemaUniquenessConstraintDependency
18
19
 
19
20
 
@@ -36,6 +37,7 @@ class AggregatedSchemaConstraintsDependency(DependencyBuilder[AggregatedConstrai
36
37
  SchemaAttributeKindConstraintDependency.build(context=context),
37
38
  SchemaNodeAttributeAddConstraintDependency.build(context=context),
38
39
  SchemaNodeRelationshipAddConstraintDependency.build(context=context),
40
+ SchemaRelationshipPeerParentConstraintDependency.build(context=context),
39
41
  ],
40
42
  db=context.db,
41
43
  branch=context.branch,
@@ -0,0 +1,8 @@
1
+ from infrahub.core.validators.relationship.peer import RelationshipPeerParentChecker
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class SchemaRelationshipPeerParentConstraintDependency(DependencyBuilder[RelationshipPeerParentChecker]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> RelationshipPeerParentChecker:
8
+ return RelationshipPeerParentChecker(db=context.db, branch=context.branch)
@@ -3,6 +3,7 @@ from .builder.constraint.node.grouped_uniqueness import NodeGroupedUniquenessCon
3
3
  from .builder.constraint.node.uniqueness import NodeAttributeUniquenessConstraintDependency
4
4
  from .builder.constraint.relationship_manager.count import RelationshipCountConstraintDependency
5
5
  from .builder.constraint.relationship_manager.peer_kind import RelationshipPeerKindConstraintDependency
6
+ from .builder.constraint.relationship_manager.peer_parent import RelationshipPeerParentConstraintDependency
6
7
  from .builder.constraint.relationship_manager.peer_relatives import RelationshipPeerRelativesConstraintDependency
7
8
  from .builder.constraint.relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
8
9
  from .builder.constraint.schema.aggregated import AggregatedSchemaConstraintsDependency
@@ -38,6 +39,7 @@ def build_component_registry() -> ComponentDependencyRegistry:
38
39
  component_registry.track_dependency(RelationshipCountConstraintDependency)
39
40
  component_registry.track_dependency(RelationshipProfilesKindConstraintDependency)
40
41
  component_registry.track_dependency(RelationshipPeerKindConstraintDependency)
42
+ component_registry.track_dependency(RelationshipPeerParentConstraintDependency)
41
43
  component_registry.track_dependency(RelationshipPeerRelativesConstraintDependency)
42
44
  component_registry.track_dependency(NodeConstraintRunnerDependency)
43
45
  component_registry.track_dependency(NodeDeleteValidatorDependency)
infrahub/git/tasks.py CHANGED
@@ -696,6 +696,7 @@ async def trigger_internal_checks(
696
696
  if (
697
697
  existing_validator.typename == InfrahubKind.REPOSITORYVALIDATOR
698
698
  and existing_validator.repository.id == model.repository
699
+ and existing_validator.label.value == validator_name
699
700
  ):
700
701
  previous_validator = existing_validator
701
702
 
infrahub/graphql/app.py CHANGED
@@ -88,6 +88,8 @@ GQL_STOP = "stop"
88
88
  ContextValue = Any | Callable[[HTTPConnection], Any]
89
89
  RootValue = Any
90
90
 
91
+ subscription_tasks = set()
92
+
91
93
 
92
94
  class InfrahubGraphQLApp:
93
95
  def __init__(
@@ -446,7 +448,9 @@ class InfrahubGraphQLApp:
446
448
 
447
449
  asyncgen = cast(AsyncGenerator[Any, None], result)
448
450
  subscriptions[operation_id] = asyncgen
449
- asyncio.create_task(self._observe_subscription(asyncgen, operation_id, websocket))
451
+ task = asyncio.create_task(self._observe_subscription(asyncgen, operation_id, websocket))
452
+ subscription_tasks.add(task)
453
+ task.add_done_callback(subscription_tasks.discard)
450
454
  return []
451
455
 
452
456
  async def _observe_subscription(
@@ -6,6 +6,7 @@ from graphql import GraphQLResolveInfo
6
6
 
7
7
  from infrahub.core import registry
8
8
  from infrahub.core.convert_object_type.conversion import InputForDestField, convert_object_type
9
+ from infrahub.core.convert_object_type.schema_mapping import get_schema_mapping
9
10
  from infrahub.core.manager import NodeManager
10
11
 
11
12
  if TYPE_CHECKING:
@@ -16,7 +17,6 @@ class ConvertObjectTypeInput(InputObjectType):
16
17
  node_id = String(required=True)
17
18
  target_kind = String(required=True)
18
19
  fields_mapping = GenericScalar(required=True) # keys are destination attributes/relationships names.
19
- branch = String(required=True)
20
20
 
21
21
 
22
22
  class ConvertObjectType(Mutation):
@@ -37,17 +37,26 @@ class ConvertObjectType(Mutation):
37
37
 
38
38
  graphql_context: GraphqlContext = info.context
39
39
 
40
+ node_to_convert = await NodeManager.get_one(
41
+ id=str(data.node_id), db=graphql_context.db, branch=graphql_context.branch
42
+ )
43
+
44
+ source_schema = registry.get_node_schema(name=node_to_convert.get_kind(), branch=graphql_context.branch)
45
+ target_schema = registry.get_node_schema(name=str(data.target_kind), branch=graphql_context.branch)
46
+
40
47
  fields_mapping: dict[str, InputForDestField] = {}
41
48
  if not isinstance(data.fields_mapping, dict):
42
49
  raise ValueError(f"Expected `fields_mapping` to be a `dict`, got {type(fields_mapping)}")
43
50
 
44
- for field, input_for_dest_field_str in data.fields_mapping.items():
45
- fields_mapping[field] = InputForDestField(**input_for_dest_field_str)
51
+ for field_name, input_for_dest_field_str in data.fields_mapping.items():
52
+ fields_mapping[field_name] = InputForDestField(**input_for_dest_field_str)
53
+
54
+ # Complete fields mapping with auto-mapping.
55
+ mapping = get_schema_mapping(source_schema=source_schema, target_schema=target_schema)
56
+ for field_name, mapping_value in mapping.items():
57
+ if mapping_value.source_field_name is not None and field_name not in fields_mapping:
58
+ fields_mapping[field_name] = InputForDestField(source_field=mapping_value.source_field_name)
46
59
 
47
- node_to_convert = await NodeManager.get_one(
48
- id=str(data.node_id), db=graphql_context.db, branch=str(data.branch)
49
- )
50
- target_schema = registry.get_node_schema(name=str(data.target_kind), branch=data.branch)
51
60
  new_node = await convert_object_type(
52
61
  node=node_to_convert,
53
62
  target_schema=target_schema,
@@ -85,6 +85,7 @@ class RelationshipAdd(Mutation):
85
85
  nodes = await _validate_peers(info=info, data=data)
86
86
  await _validate_permissions(info=info, source_node=source, peers=nodes)
87
87
  await _validate_peer_types(info=info, data=data, source_node=source, peers=nodes)
88
+ await _validate_peer_parents(info=info, data=data, source_node=source, peers=nodes)
88
89
 
89
90
  # This has to be done after validating the permissions
90
91
  await apply_external_context(graphql_context=graphql_context, context_input=context)
@@ -406,6 +407,37 @@ async def _validate_peer_types(
406
407
  )
407
408
 
408
409
 
410
+ async def _validate_peer_parents(
411
+ info: GraphQLResolveInfo, data: RelationshipNodesInput, source_node: Node, peers: dict[str, Node]
412
+ ) -> None:
413
+ relationship_name = str(data.name)
414
+ rel_schema = source_node.get_schema().get_relationship(name=relationship_name)
415
+ if not rel_schema.common_parent:
416
+ return
417
+
418
+ graphql_context: GraphqlContext = info.context
419
+
420
+ source_node_parent = await source_node.get_parent_relationship_peer(
421
+ db=graphql_context.db, name=rel_schema.common_parent
422
+ )
423
+ if not source_node_parent:
424
+ # If the schema is properly validated we are not expecting this to happen
425
+ raise ValidationError(f"Node {source_node.id} ({source_node.get_kind()!r}) does not have a parent peer")
426
+
427
+ parents: set[str] = {source_node_parent.id}
428
+ for peer in peers.values():
429
+ peer_parent = await peer.get_parent_relationship_peer(db=graphql_context.db, name=rel_schema.common_parent)
430
+ if not peer_parent:
431
+ # If the schema is properly validated we are not expecting this to happen
432
+ raise ValidationError(f"Peer {peer.id} ({peer.get_kind()!r}) does not have a parent peer")
433
+ parents.add(peer_parent.id)
434
+
435
+ if len(parents) > 1:
436
+ raise ValidationError(
437
+ f"Cannot relate {source_node.id!r} to '{relationship_name}' peers that do not have the same parent"
438
+ )
439
+
440
+
409
441
  async def _collect_current_peers(
410
442
  info: GraphQLResolveInfo, data: RelationshipNodesInput, source_node: Node
411
443
  ) -> dict[str, RelationshipPeerData]:
@@ -12,13 +12,12 @@ class FieldsMapping(ObjectType):
12
12
 
13
13
  async def fields_mapping_type_conversion_resolver(
14
14
  root: dict, # noqa: ARG001
15
- info: GraphQLResolveInfo, # noqa: ARG001
15
+ info: GraphQLResolveInfo,
16
16
  source_kind: str,
17
17
  target_kind: str,
18
- branch: str,
19
18
  ) -> dict:
20
- source_schema = registry.get_node_schema(name=source_kind, branch=branch)
21
- target_schema = registry.get_node_schema(name=target_kind, branch=branch)
19
+ source_schema = registry.get_node_schema(name=source_kind, branch=info.context.branch)
20
+ target_schema = registry.get_node_schema(name=target_kind, branch=info.context.branch)
22
21
 
23
22
  mapping = get_schema_mapping(source_schema=source_schema, target_schema=target_schema)
24
23
  mapping_dict = {field_name: model.model_dump(mode="json") for field_name, model in mapping.items()}
@@ -29,7 +28,6 @@ FieldsMappingTypeConversion = Field(
29
28
  FieldsMapping,
30
29
  source_kind=String(),
31
30
  target_kind=String(),
32
- branch=String(),
33
31
  description="Retrieve fields mapping for converting object type",
34
32
  resolver=fields_mapping_type_conversion_resolver,
35
33
  required=True,
@@ -1,5 +1,3 @@
1
- from infrahub import lock
2
- from infrahub.core.registry import registry
3
1
  from infrahub.message_bus import messages
4
2
  from infrahub.services import InfrahubServices
5
3
  from infrahub.tasks.registry import refresh_branches
@@ -24,8 +22,7 @@ async def rebased_branch(message: messages.RefreshRegistryRebasedBranch, service
24
22
  )
25
23
  return
26
24
 
27
- async with lock.registry.local_schema_lock():
28
- service.log.info("Refreshing rebased branch")
25
+ async with service.database.start_session(read_only=True) as db:
26
+ await refresh_branches(db=db)
29
27
 
30
- async with service.database.start_session(read_only=True) as db:
31
- registry.branch[message.branch] = await registry.branch_object.get_by_name(name=message.branch, db=db)
28
+ await service.component.refresh_schema_hash()
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class NumberPoolLockDefinition:
6
+ pool_id: str
7
+
8
+ @property
9
+ def lock_name(self) -> str:
10
+ return f"number-pool-creation-{self.pool_id}"
11
+
12
+ @property
13
+ def namespace_name(self) -> str:
14
+ return "number-pool"