infrahub-server 1.3.0a0__py3-none-any.whl → 1.3.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. infrahub/actions/tasks.py +4 -11
  2. infrahub/branch/__init__.py +0 -0
  3. infrahub/branch/tasks.py +29 -0
  4. infrahub/branch/triggers.py +22 -0
  5. infrahub/cli/db.py +2 -2
  6. infrahub/computed_attribute/gather.py +3 -1
  7. infrahub/computed_attribute/tasks.py +23 -29
  8. infrahub/core/attribute.py +3 -3
  9. infrahub/core/constants/__init__.py +10 -0
  10. infrahub/core/constants/database.py +1 -0
  11. infrahub/core/constants/infrahubkind.py +2 -0
  12. infrahub/core/convert_object_type/conversion.py +1 -1
  13. infrahub/core/diff/query/save.py +67 -40
  14. infrahub/core/diff/query/time_range_query.py +0 -1
  15. infrahub/core/graph/__init__.py +1 -1
  16. infrahub/core/migrations/graph/__init__.py +6 -0
  17. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +0 -2
  18. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +662 -0
  19. infrahub/core/migrations/graph/m030_illegal_edges.py +82 -0
  20. infrahub/core/migrations/query/attribute_add.py +13 -9
  21. infrahub/core/migrations/query/attribute_rename.py +2 -4
  22. infrahub/core/migrations/query/delete_element_in_schema.py +16 -11
  23. infrahub/core/migrations/query/node_duplicate.py +16 -15
  24. infrahub/core/migrations/query/relationship_duplicate.py +16 -12
  25. infrahub/core/migrations/schema/node_attribute_remove.py +1 -2
  26. infrahub/core/migrations/schema/node_remove.py +16 -14
  27. infrahub/core/node/__init__.py +74 -14
  28. infrahub/core/node/base.py +1 -1
  29. infrahub/core/node/resource_manager/ip_address_pool.py +6 -2
  30. infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -2
  31. infrahub/core/node/resource_manager/number_pool.py +31 -5
  32. infrahub/core/node/standard.py +6 -1
  33. infrahub/core/path.py +1 -1
  34. infrahub/core/protocols.py +10 -0
  35. infrahub/core/query/node.py +1 -1
  36. infrahub/core/query/relationship.py +4 -6
  37. infrahub/core/query/standard_node.py +19 -5
  38. infrahub/core/relationship/constraints/peer_relatives.py +72 -0
  39. infrahub/core/relationship/model.py +1 -1
  40. infrahub/core/schema/attribute_parameters.py +129 -5
  41. infrahub/core/schema/attribute_schema.py +62 -14
  42. infrahub/core/schema/basenode_schema.py +2 -2
  43. infrahub/core/schema/definitions/core/__init__.py +16 -2
  44. infrahub/core/schema/definitions/core/group.py +45 -0
  45. infrahub/core/schema/definitions/core/resource_pool.py +29 -0
  46. infrahub/core/schema/definitions/internal.py +25 -4
  47. infrahub/core/schema/generated/attribute_schema.py +12 -5
  48. infrahub/core/schema/generated/relationship_schema.py +6 -1
  49. infrahub/core/schema/manager.py +7 -2
  50. infrahub/core/schema/schema_branch.py +69 -5
  51. infrahub/core/validators/__init__.py +8 -0
  52. infrahub/core/validators/attribute/choices.py +0 -1
  53. infrahub/core/validators/attribute/enum.py +0 -1
  54. infrahub/core/validators/attribute/kind.py +0 -1
  55. infrahub/core/validators/attribute/length.py +0 -1
  56. infrahub/core/validators/attribute/min_max.py +118 -0
  57. infrahub/core/validators/attribute/number_pool.py +106 -0
  58. infrahub/core/validators/attribute/optional.py +0 -2
  59. infrahub/core/validators/attribute/regex.py +0 -1
  60. infrahub/core/validators/enum.py +5 -0
  61. infrahub/core/validators/tasks.py +1 -1
  62. infrahub/database/__init__.py +16 -4
  63. infrahub/database/validation.py +100 -0
  64. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  65. infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
  66. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  67. infrahub/dependencies/registry.py +2 -0
  68. infrahub/events/models.py +1 -1
  69. infrahub/git/base.py +5 -3
  70. infrahub/git/integrator.py +102 -3
  71. infrahub/graphql/mutations/main.py +1 -1
  72. infrahub/graphql/mutations/resource_manager.py +54 -6
  73. infrahub/graphql/queries/resource_manager.py +7 -1
  74. infrahub/graphql/queries/task.py +10 -0
  75. infrahub/graphql/resolvers/many_relationship.py +1 -1
  76. infrahub/graphql/resolvers/resolver.py +2 -2
  77. infrahub/graphql/resolvers/single_relationship.py +1 -1
  78. infrahub/graphql/types/task_log.py +3 -2
  79. infrahub/menu/menu.py +8 -7
  80. infrahub/message_bus/operations/refresh/registry.py +3 -3
  81. infrahub/patch/queries/delete_duplicated_edges.py +40 -29
  82. infrahub/pools/number.py +5 -3
  83. infrahub/pools/registration.py +22 -0
  84. infrahub/pools/tasks.py +56 -0
  85. infrahub/schema/__init__.py +0 -0
  86. infrahub/schema/tasks.py +27 -0
  87. infrahub/schema/triggers.py +23 -0
  88. infrahub/task_manager/task.py +44 -4
  89. infrahub/trigger/catalogue.py +4 -0
  90. infrahub/trigger/models.py +5 -4
  91. infrahub/trigger/setup.py +26 -2
  92. infrahub/trigger/tasks.py +1 -1
  93. infrahub/types.py +6 -0
  94. infrahub/webhook/tasks.py +6 -9
  95. infrahub/workflows/catalogue.py +27 -1
  96. infrahub_sdk/client.py +43 -10
  97. infrahub_sdk/node/__init__.py +39 -0
  98. infrahub_sdk/node/attribute.py +122 -0
  99. infrahub_sdk/node/constants.py +21 -0
  100. infrahub_sdk/{node.py → node/node.py} +50 -749
  101. infrahub_sdk/node/parsers.py +15 -0
  102. infrahub_sdk/node/property.py +24 -0
  103. infrahub_sdk/node/related_node.py +266 -0
  104. infrahub_sdk/node/relationship.py +302 -0
  105. infrahub_sdk/protocols.py +112 -0
  106. infrahub_sdk/protocols_base.py +34 -2
  107. infrahub_sdk/query_groups.py +13 -2
  108. infrahub_sdk/schema/main.py +1 -0
  109. infrahub_sdk/schema/repository.py +16 -0
  110. infrahub_sdk/spec/object.py +1 -1
  111. infrahub_sdk/store.py +1 -1
  112. infrahub_sdk/testing/schemas/car_person.py +1 -0
  113. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/METADATA +3 -3
  114. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/RECORD +122 -100
  115. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/WHEEL +1 -1
  116. infrahub_testcontainers/container.py +239 -64
  117. infrahub_testcontainers/docker-compose-cluster.test.yml +321 -0
  118. infrahub_testcontainers/docker-compose.test.yml +1 -0
  119. infrahub_testcontainers/helpers.py +15 -1
  120. infrahub_testcontainers/plugin.py +9 -0
  121. infrahub/patch/queries/consolidate_duplicated_nodes.py +0 -106
  122. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/LICENSE.txt +0 -0
  123. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/entry_points.txt +0 -0
@@ -16,6 +16,7 @@ from infrahub.core.constants import (
16
16
  BranchSupportType,
17
17
  ComputedAttributeKind,
18
18
  InfrahubKind,
19
+ NumberPoolType,
19
20
  RelationshipCardinality,
20
21
  RelationshipKind,
21
22
  )
@@ -30,6 +31,7 @@ from infrahub.core.schema import (
30
31
  RelationshipSchema,
31
32
  TemplateSchema,
32
33
  )
34
+ from infrahub.core.schema.attribute_parameters import NumberPoolParameters
33
35
  from infrahub.core.timestamp import Timestamp
34
36
  from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
35
37
  from infrahub.types import ATTRIBUTE_TYPES
@@ -254,6 +256,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
254
256
  within the create code.
255
257
  """
256
258
 
259
+ number_pool_parameters: NumberPoolParameters | None = None
260
+ if attribute.schema.kind == "NumberPool" and isinstance(attribute.schema.parameters, NumberPoolParameters):
261
+ attribute.from_pool = {"id": attribute.schema.parameters.number_pool_id}
262
+ attribute.is_default = False
263
+ number_pool_parameters = attribute.schema.parameters
264
+
257
265
  if not attribute.from_pool:
258
266
  return
259
267
 
@@ -262,19 +270,25 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
262
270
  db=db, id=attribute.from_pool["id"], kind=CoreNumberPool
263
271
  )
264
272
  except NodeNotFoundError:
265
- errors.append(
266
- ValidationError(
267
- {f"{attribute.name}.from_pool": f"The pool requested {attribute.from_pool} was not found."}
273
+ if number_pool_parameters:
274
+ number_pool = await self._create_number_pool(
275
+ db=db, attribute=attribute, number_pool_parameters=number_pool_parameters
268
276
  )
269
- )
270
- return
277
+
278
+ else:
279
+ errors.append(
280
+ ValidationError(
281
+ {f"{attribute.name}.from_pool": f"The pool requested {attribute.from_pool} was not found."}
282
+ )
283
+ )
284
+ return
271
285
 
272
286
  if (
273
287
  number_pool.node.value in [self._schema.kind] + self._schema.inherit_from
274
288
  and number_pool.node_attribute.value == attribute.name
275
289
  ):
276
290
  try:
277
- next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self)
291
+ next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self, attribute=attribute)
278
292
  except PoolExhaustedError:
279
293
  errors.append(
280
294
  ValidationError({f"{attribute.name}.from_pool": f"The pool {number_pool.node.value} is exhausted."})
@@ -292,6 +306,36 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
292
306
  )
293
307
  )
294
308
 
309
+ async def _create_number_pool(
310
+ self, db: InfrahubDatabase, attribute: BaseAttribute, number_pool_parameters: NumberPoolParameters
311
+ ) -> CoreNumberPool:
312
+ schema = db.schema.get_node_schema(name="CoreNumberPool", duplicate=False)
313
+
314
+ pool_node = self._schema.kind
315
+ schema_attribute = self._schema.get_attribute(attribute.schema.name)
316
+ if schema_attribute.inherited:
317
+ for generic_name in self._schema.inherit_from:
318
+ generic_node = db.schema.get_generic_schema(name=generic_name, duplicate=False)
319
+ if attribute.schema.name in generic_node.attribute_names:
320
+ pool_node = generic_node.kind
321
+ break
322
+
323
+ number_pool = await Node.init(db=db, schema=schema, branch=self._branch)
324
+ await number_pool.new(
325
+ db=db,
326
+ id=number_pool_parameters.number_pool_id,
327
+ name=f"{pool_node}.{attribute.schema.name} [{number_pool_parameters.number_pool_id}]",
328
+ node=pool_node,
329
+ node_attribute=attribute.schema.name,
330
+ start_range=number_pool_parameters.start_range,
331
+ end_range=number_pool_parameters.end_range,
332
+ pool_type=NumberPoolType.SCHEMA.value,
333
+ )
334
+ await number_pool.save(db=db)
335
+ # Do a lookup of the number pool to get the correct mapped type from the registry
336
+ # without this we don't get access to the .get_resource() method.
337
+ return await registry.manager.get_one_by_id_or_default_filter(db=db, id=number_pool.id, kind=CoreNumberPool)
338
+
295
339
  async def handle_object_template(self, fields: dict, db: InfrahubDatabase, errors: list) -> None:
296
340
  """Fill the `fields` parameters with values from an object template if one is in use."""
297
341
  object_template_field = fields.get(OBJECT_TEMPLATE_RELATIONSHIP_NAME)
@@ -376,6 +420,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
376
420
  self._computed_jinja2_attributes.append(mandatory_attr)
377
421
  continue
378
422
 
423
+ if mandatory_attribute.kind == "NumberPool":
424
+ continue
425
+
379
426
  errors.append(
380
427
  ValidationError({mandatory_attr: f"{mandatory_attr} is mandatory for {self.get_kind()}"})
381
428
  )
@@ -392,6 +439,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
392
439
  # -------------------------------------------
393
440
  # Generate Attribute and Relationship and assign them
394
441
  # -------------------------------------------
442
+ errors.extend(await self._process_fields_relationships(fields=fields, db=db))
443
+ errors.extend(await self._process_fields_attributes(fields=fields, db=db))
444
+
445
+ if errors:
446
+ raise ValidationError(errors)
447
+
448
+ # Check if any post processor have been defined
449
+ # A processor can be used for example to assigne a default value
450
+ for name in self._attributes + self._relationships:
451
+ if hasattr(self, f"process_{name}"):
452
+ await getattr(self, f"process_{name}")(db=db)
453
+
454
+ async def _process_fields_relationships(self, fields: dict, db: InfrahubDatabase) -> list[ValidationError]:
455
+ errors: list[ValidationError] = []
456
+
395
457
  for rel_schema in self._schema.relationships:
396
458
  self._relationships.append(rel_schema.name)
397
459
 
@@ -413,6 +475,11 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
413
475
  except ValidationError as exc:
414
476
  errors.append(exc)
415
477
 
478
+ return errors
479
+
480
+ async def _process_fields_attributes(self, fields: dict, db: InfrahubDatabase) -> list[ValidationError]:
481
+ errors: list[ValidationError] = []
482
+
416
483
  for attr_schema in self._schema.attributes:
417
484
  self._attributes.append(attr_schema.name)
418
485
  if not self._existing and attr_schema.name in self._computed_jinja2_attributes:
@@ -441,14 +508,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
441
508
  except ValidationError as exc:
442
509
  errors.append(exc)
443
510
 
444
- if errors:
445
- raise ValidationError(errors)
446
-
447
- # Check if any post processor have been defined
448
- # A processor can be used for example to assigne a default value
449
- for name in self._attributes + self._relationships:
450
- if hasattr(self, f"process_{name}"):
451
- await getattr(self, f"process_{name}")(db=db)
511
+ return errors
452
512
 
453
513
  async def _process_macros(self, db: InfrahubDatabase) -> None:
454
514
  schema_branch = db.schema.get_schema_branch(self._branch.name)
@@ -52,7 +52,7 @@ class BaseNodeOptions(BaseOptions):
52
52
 
53
53
 
54
54
  class ObjectNodeMeta(BaseNodeMeta):
55
- def __new__(mcs, name_, bases, namespace, **options): # noqa: N804
55
+ def __new__(mcs, name_, bases, namespace, **options):
56
56
  # Note: it's safe to pass options as keyword arguments as they are still type-checked by NodeOptions.
57
57
 
58
58
  # We create this type, to then overload it with the dataclass attrs
@@ -81,11 +81,15 @@ class CoreIPAddressPool(Node):
81
81
  return node
82
82
 
83
83
  async def get_next(self, db: InfrahubDatabase, prefixlen: int | None = None) -> IPAddressType:
84
- # Measure utilization of all prefixes identified as resources
85
84
  resources = await self.resources.get_peers(db=db) # type: ignore[attr-defined]
86
85
  ip_namespace = await self.ip_namespace.get_peer(db=db) # type: ignore[attr-defined]
87
86
 
88
- for resource in resources.values():
87
+ try:
88
+ weighted_resources = sorted(resources.values(), key=lambda r: r.allocation_weight.value or 0, reverse=True)
89
+ except AttributeError:
90
+ weighted_resources = list(resources.values())
91
+
92
+ for resource in weighted_resources:
89
93
  ip_prefix = ipaddress.ip_network(resource.prefix.value) # type: ignore[attr-defined]
90
94
  prefix_length = prefixlen or ip_prefix.prefixlen
91
95
 
@@ -88,11 +88,15 @@ class CoreIPPrefixPool(Node):
88
88
  return node
89
89
 
90
90
  async def get_next(self, db: InfrahubDatabase, prefixlen: int) -> IPNetworkType:
91
- # Measure utilization of all prefixes identified as resources
92
91
  resources = await self.resources.get_peers(db=db) # type: ignore[attr-defined]
93
92
  ip_namespace = await self.ip_namespace.get_peer(db=db) # type: ignore[attr-defined]
94
93
 
95
- for resource in resources.values():
94
+ try:
95
+ weighted_resources = sorted(resources.values(), key=lambda r: r.allocation_weight.value or 0, reverse=True)
96
+ except AttributeError:
97
+ weighted_resources = list(resources.values())
98
+
99
+ for resource in weighted_resources:
96
100
  subnets = await get_subnets(
97
101
  db=db,
98
102
  ip_prefix=ipaddress.ip_network(resource.prefix.value), # type: ignore[attr-defined]
@@ -2,22 +2,44 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from infrahub.core import registry
5
6
  from infrahub.core.query.resource_manager import NumberPoolGetReserved, NumberPoolGetUsed, NumberPoolSetReserved
7
+ from infrahub.core.schema.attribute_parameters import NumberAttributeParameters
6
8
  from infrahub.exceptions import PoolExhaustedError
7
9
 
8
10
  from .. import Node
9
11
 
10
12
  if TYPE_CHECKING:
13
+ from infrahub.core.attribute import BaseAttribute
11
14
  from infrahub.core.branch import Branch
12
15
  from infrahub.database import InfrahubDatabase
13
16
 
14
17
 
15
18
  class CoreNumberPool(Node):
19
+ def get_attribute_nb_excluded_values(self) -> int:
20
+ """
21
+ Returns the number of excluded values for the attribute of the number pool.
22
+ """
23
+
24
+ pool_node = registry.schema.get(name=self.node.value) # type: ignore [attr-defined]
25
+ attribute = [attribute for attribute in pool_node.attributes if attribute.name == self.node_attribute.value][0] # type: ignore [attr-defined]
26
+ if not isinstance(attribute.parameters, NumberAttributeParameters):
27
+ return 0
28
+
29
+ sum_excluded_values = 0
30
+ excluded_ranges = attribute.parameters.get_excluded_ranges()
31
+ for start_range, end_range in excluded_ranges:
32
+ sum_excluded_values += end_range - start_range + 1
33
+
34
+ res = len(attribute.parameters.get_excluded_single_values()) + sum_excluded_values
35
+ return res
36
+
16
37
  async def get_resource(
17
38
  self,
18
39
  db: InfrahubDatabase,
19
40
  branch: Branch,
20
41
  node: Node,
42
+ attribute: BaseAttribute,
21
43
  identifier: str | None = None,
22
44
  ) -> int:
23
45
  identifier = identifier or node.get_id()
@@ -31,23 +53,24 @@ class CoreNumberPool(Node):
31
53
  return reservation
32
54
 
33
55
  # If we have not returned a value we need to find one if avaiable
34
- number = await self.get_next(db=db, branch=branch)
56
+ number = await self.get_next(db=db, branch=branch, attribute=attribute)
35
57
 
36
58
  query_set = await NumberPoolSetReserved.init(
37
59
  db=db, pool_id=self.get_id(), identifier=identifier, reserved=number
38
60
  )
39
61
  await query_set.execute(db=db)
40
-
41
62
  return number
42
63
 
43
- async def get_next(self, db: InfrahubDatabase, branch: Branch) -> int:
64
+ async def get_next(self, db: InfrahubDatabase, branch: Branch, attribute: BaseAttribute) -> int:
44
65
  query = await NumberPoolGetUsed.init(db=db, branch=branch, pool=self, branch_agnostic=True)
45
66
  await query.execute(db=db)
46
67
  taken = [result.get_as_optional_type("av.value", return_type=int) for result in query.results]
68
+ parameters = attribute.schema.parameters
47
69
  next_number = find_next_free(
48
70
  start=self.start_range.value, # type: ignore[attr-defined]
49
71
  end=self.end_range.value, # type: ignore[attr-defined]
50
72
  taken=taken,
73
+ parameters=parameters if isinstance(parameters, NumberAttributeParameters) else None,
51
74
  )
52
75
  if next_number is None:
53
76
  raise PoolExhaustedError("There are no more values available in this pool.")
@@ -55,12 +78,15 @@ class CoreNumberPool(Node):
55
78
  return next_number
56
79
 
57
80
 
58
- def find_next_free(start: int, end: int, taken: list[int | None]) -> int | None:
81
+ def find_next_free(
82
+ start: int, end: int, taken: list[int | None], parameters: NumberAttributeParameters | None
83
+ ) -> int | None:
59
84
  used_numbers = [number for number in taken if number is not None]
60
85
  used_set = set(used_numbers)
61
86
 
62
87
  for num in range(start, end + 1):
63
88
  if num not in used_set:
64
- return num
89
+ if parameters is None or parameters.is_valid_value(num):
90
+ return num
65
91
 
66
92
  return None
@@ -210,7 +210,12 @@ class StandardNode(BaseModel):
210
210
 
211
211
  @classmethod
212
212
  async def get_list(
213
- cls, db: InfrahubDatabase, limit: int = 1000, ids: list[str] | None = None, name: str | None = None, **kwargs
213
+ cls,
214
+ db: InfrahubDatabase,
215
+ limit: int = 1000,
216
+ ids: list[str] | None = None,
217
+ name: str | None = None,
218
+ **kwargs: dict[str, Any],
214
219
  ) -> list[Self]:
215
220
  query: Query = await StandardNodeGetListQuery.init(
216
221
  db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs
infrahub/core/path.py CHANGED
@@ -125,7 +125,7 @@ class SchemaPath(InfrahubPath):
125
125
  if self.field_name:
126
126
  identifier += f"/{self.field_name}"
127
127
 
128
- if self.property_name and not self.path_type == SchemaPathType.NODE:
128
+ if self.property_name and self.path_type != SchemaPathType.NODE:
129
129
  identifier += f"/{self.property_name}"
130
130
 
131
131
  return identifier
@@ -227,6 +227,10 @@ class CoreWebhook(CoreNode):
227
227
  validate_certificates: BooleanOptional
228
228
 
229
229
 
230
+ class CoreWeightedPoolResource(CoreNode):
231
+ allocation_weight: IntegerOptional
232
+
233
+
230
234
  class LineageOwner(CoreNode):
231
235
  pass
232
236
 
@@ -451,6 +455,7 @@ class CoreNumberPool(CoreResourcePool, LineageSource):
451
455
  node_attribute: String
452
456
  start_range: Integer
453
457
  end_range: Integer
458
+ pool_type: Enum
454
459
 
455
460
 
456
461
  class CoreObjectPermission(CoreBasePermission):
@@ -493,6 +498,11 @@ class CoreRepository(LineageOwner, LineageSource, CoreGenericRepository, CoreTas
493
498
  commit: StringOptional
494
499
 
495
500
 
501
+ class CoreRepositoryGroup(CoreGroup):
502
+ content: Dropdown
503
+ repository: RelationshipManager
504
+
505
+
496
506
  class CoreRepositoryValidator(CoreValidator):
497
507
  repository: RelationshipManager
498
508
 
@@ -1469,7 +1469,7 @@ class NodeGetHierarchyQuery(Query):
1469
1469
 
1470
1470
  clean_filters = extract_field_filters(field_name=self.direction.value, filters=self.filters)
1471
1471
 
1472
- if clean_filters and "id" in clean_filters or "ids" in clean_filters:
1472
+ if (clean_filters and "id" in clean_filters) or "ids" in clean_filters:
1473
1473
  where_clause.append("peer.uuid IN $peer_ids")
1474
1474
  self.params["peer_ids"] = clean_filters.get("ids", [])
1475
1475
  if clean_filters.get("id", None):
@@ -217,8 +217,7 @@ class RelationshipQuery(Query):
217
217
  )
218
218
  source_query_match = """
219
219
  MATCH (s:Node { uuid: $source_id })
220
- CALL {
221
- WITH s
220
+ CALL (s) {
222
221
  MATCH (s)-[r:IS_PART_OF]->(:Root)
223
222
  WHERE %(source_filter)s
224
223
  RETURN r.status = "active" AS s_is_active
@@ -246,8 +245,7 @@ class RelationshipQuery(Query):
246
245
  )
247
246
  destination_query_match = """
248
247
  MATCH (d:Node { uuid: $destination_id })
249
- CALL {
250
- WITH d
248
+ CALL (d) {
251
249
  MATCH (d)-[r:IS_PART_OF]->(:Root)
252
250
  WHERE %(destination_filter)s
253
251
  RETURN r.status = "active" AS d_is_active
@@ -678,7 +676,7 @@ class RelationshipGetPeerQuery(Query):
678
676
  where_clause = ['all(r IN rels WHERE r.status = "active")']
679
677
  clean_filters = extract_field_filters(field_name=self.schema.name, filters=self.filters)
680
678
 
681
- if clean_filters and "id" in clean_filters or "ids" in clean_filters:
679
+ if (clean_filters and "id" in clean_filters) or "ids" in clean_filters:
682
680
  where_clause.append("peer.uuid IN $peer_ids")
683
681
  self.params["peer_ids"] = clean_filters.get("ids", [])
684
682
  if clean_filters.get("id", None):
@@ -1037,7 +1035,7 @@ class RelationshipDeleteAllQuery(Query):
1037
1035
  self.node_id = node_id
1038
1036
  super().__init__(**kwargs)
1039
1037
 
1040
- async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
1038
+ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
1041
1039
  self.params["source_id"] = kwargs["node_id"]
1042
1040
  self.params["branch"] = self.branch.name
1043
1041
 
@@ -3,28 +3,42 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from infrahub.core.query import Query, QueryType
6
+ from infrahub.exceptions import InitializationError
6
7
 
7
8
  if TYPE_CHECKING:
9
+ from uuid import UUID
10
+
8
11
  from infrahub.core.node.standard import StandardNode
9
12
  from infrahub.database import InfrahubDatabase
10
13
 
11
14
 
12
15
  class StandardNodeQuery(Query):
13
16
  def __init__(
14
- self, node: StandardNode = None, node_id: str | None = None, node_db_id: int | None = None, **kwargs: Any
15
- ):
16
- self.node = node
17
+ self,
18
+ node: StandardNode | None = None,
19
+ node_id: UUID | None = None,
20
+ node_db_id: str | None = None,
21
+ **kwargs: Any,
22
+ ) -> None:
23
+ self._node = node
17
24
  self.node_id = node_id
18
25
  self.node_db_id = node_db_id
19
26
 
20
- if not self.node_id and self.node:
27
+ if not self.node_id and self._node:
21
28
  self.node_id = self.node.uuid
22
29
 
23
- if not self.node_db_id and self.node:
30
+ if not self.node_db_id and self._node:
24
31
  self.node_db_id = self.node.id
25
32
 
26
33
  super().__init__(**kwargs)
27
34
 
35
+ @property
36
+ def node(self) -> StandardNode:
37
+ if self._node:
38
+ return self._node
39
+
40
+ raise InitializationError("The query is not initialized with a node")
41
+
28
42
 
29
43
  class RootNodeCreateQuery(StandardNodeQuery):
30
44
  name = "standard_node_create"
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Mapping
5
+
6
+ from infrahub.core.constants import RelationshipCardinality
7
+ from infrahub.exceptions import ValidationError
8
+
9
+ from .interface import RelationshipManagerConstraintInterface
10
+
11
+ if TYPE_CHECKING:
12
+ from infrahub.core.branch import Branch
13
+ from infrahub.core.node import Node
14
+ from infrahub.core.schema import MainSchemaTypes, NonGenericSchemaTypes
15
+ from infrahub.database import InfrahubDatabase
16
+
17
+ from ..model import RelationshipManager
18
+
19
+
20
+ @dataclass
21
+ class NodeToValidate:
22
+ uuid: str
23
+ relative_uuids: set[str]
24
+ schema: NonGenericSchemaTypes
25
+
26
+
27
+ class RelationshipPeerRelativesConstraint(RelationshipManagerConstraintInterface):
28
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
29
+ self.db = db
30
+ self.branch = branch
31
+
32
+ async def _check_relationship_peers_relatives(
33
+ self,
34
+ relm: RelationshipManager,
35
+ node_schema: MainSchemaTypes,
36
+ peers: Mapping[str, Node],
37
+ relationship_name: str,
38
+ ) -> None:
39
+ """Validate that all peers of a given `relm` have the same set of relatives (aka peers) for the given `relationship_name`."""
40
+ nodes_to_validate: list[NodeToValidate] = []
41
+
42
+ for peer in peers.values():
43
+ peer_schema = peer.get_schema()
44
+ peer_relm: RelationshipManager = getattr(peer, relationship_name)
45
+ peer_relm_peers = await peer_relm.get_peers(db=self.db)
46
+
47
+ nodes_to_validate.append(
48
+ NodeToValidate(
49
+ uuid=peer.id, relative_uuids={n.id for n in peer_relm_peers.values()}, schema=peer_schema
50
+ )
51
+ )
52
+
53
+ relative_uuids = nodes_to_validate[0].relative_uuids
54
+ for node in nodes_to_validate[1:]:
55
+ if node.relative_uuids != relative_uuids:
56
+ raise ValidationError(
57
+ f"All the elements of the '{relm.name}' relationship on node {node.uuid} ({node_schema.kind}) must have the same set of peers "
58
+ f"for their '{node.schema.kind}.{relationship_name}' relationship"
59
+ )
60
+
61
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None:
62
+ if relm.schema.cardinality != RelationshipCardinality.MANY or not relm.schema.common_relatives:
63
+ return
64
+
65
+ peers = await relm.get_peers(db=self.db)
66
+ if not peers:
67
+ return
68
+
69
+ for rel_name in relm.schema.common_relatives:
70
+ await self._check_relationship_peers_relatives(
71
+ relm=relm, node_schema=node_schema, peers=peers, relationship_name=rel_name
72
+ )
@@ -494,7 +494,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
494
494
  peer_fields = {
495
495
  key: value
496
496
  for key, value in fields.items()
497
- if not key.startswith(PREFIX_PROPERTY) or not key == "__typename"
497
+ if not key.startswith(PREFIX_PROPERTY) or key != "__typename"
498
498
  }
499
499
  rel_fields = {
500
500
  key.replace(PREFIX_PROPERTY, ""): value