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
@@ -5,6 +5,7 @@ import hashlib
5
5
  from collections import defaultdict
6
6
  from itertools import chain, combinations
7
7
  from typing import Any
8
+ from uuid import uuid4
8
9
 
9
10
  from infrahub_sdk.template import Jinja2Template
10
11
  from infrahub_sdk.template.exceptions import JinjaTemplateError, JinjaTemplateOperationViolationError
@@ -49,6 +50,7 @@ from infrahub.core.schema import (
49
50
  SchemaRoot,
50
51
  TemplateSchema,
51
52
  )
53
+ from infrahub.core.schema.attribute_parameters import NumberPoolParameters
52
54
  from infrahub.core.schema.attribute_schema import get_attribute_schema_class_for_kind
53
55
  from infrahub.core.schema.definitions.core import core_profile_schema_definition
54
56
  from infrahub.core.validators import CONSTRAINT_VALIDATOR_MAP
@@ -89,7 +91,7 @@ class SchemaBranch:
89
91
  self.templates = data.get("templates", {})
90
92
 
91
93
  @classmethod
92
- def validate(cls, data: Any) -> Self: # noqa: ARG003
94
+ def validate(cls, data: Any) -> Self:
93
95
  if isinstance(data, cls):
94
96
  return data
95
97
  if isinstance(data, dict):
@@ -451,7 +453,7 @@ class SchemaBranch:
451
453
  if isinstance(node, NodeSchema | ProfileSchema | TemplateSchema):
452
454
  return node.generate_fields_for_display_label()
453
455
 
454
- fields: dict[str, str | None | dict[str, None]] = {}
456
+ fields: dict[str, str | dict[str, None] | None] = {}
455
457
  if isinstance(node, GenericSchema):
456
458
  for child_node_name in node.used_by:
457
459
  child_node = self.get(name=child_node_name, duplicate=False)
@@ -518,6 +520,7 @@ class SchemaBranch:
518
520
  self.validate_names()
519
521
  self.validate_kinds()
520
522
  self.validate_computed_attributes()
523
+ self.validate_attribute_parameters()
521
524
  self.validate_default_values()
522
525
  self.validate_count_against_cardinality()
523
526
  self.validate_identifiers()
@@ -994,6 +997,65 @@ class SchemaBranch:
994
997
  raise ValueError(
995
998
  f"{node.kind}: Relationship {rel.name!r} is referring an invalid peer {rel.peer!r}"
996
999
  ) from None
1000
+ if rel.common_relatives:
1001
+ peer_schema = self.get(name=rel.peer, duplicate=False)
1002
+ for common_relatives_rel_name in rel.common_relatives:
1003
+ if common_relatives_rel_name not in peer_schema.relationship_names:
1004
+ raise ValueError(
1005
+ f"{node.kind}: Relationship {rel.name!r} set 'common_relatives' with invalid relationship from '{rel.peer}'"
1006
+ ) from None
1007
+
1008
+ def validate_attribute_parameters(self) -> None:
1009
+ for name in self.generics.keys():
1010
+ generic_schema = self.get_generic(name=name, duplicate=False)
1011
+ for attribute in generic_schema.attributes:
1012
+ if (
1013
+ attribute.kind == "NumberPool"
1014
+ and isinstance(attribute.parameters, NumberPoolParameters)
1015
+ and not attribute.parameters.number_pool_id
1016
+ ):
1017
+ attribute.parameters.number_pool_id = str(uuid4())
1018
+
1019
+ for name in self.nodes.keys():
1020
+ node_schema = self.get_node(name=name, duplicate=False)
1021
+ 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
+ ):
1027
+ self._validate_number_pool_parameters(
1028
+ node_schema=node_schema, attribute=attribute, number_pool_parameters=attribute.parameters
1029
+ )
1030
+
1031
+ def _validate_number_pool_parameters(
1032
+ self, node_schema: NodeSchema, attribute: AttributeSchema, number_pool_parameters: NumberPoolParameters
1033
+ ) -> None:
1034
+ if attribute.optional:
1035
+ raise ValidationError(f"{node_schema.kind}.{attribute.name} is a NumberPool it can't be optional")
1036
+
1037
+ if not attribute.read_only:
1038
+ raise ValidationError(
1039
+ f"{node_schema.kind}.{attribute.name} is a NumberPool it has to be a read_only attribute"
1040
+ )
1041
+
1042
+ if attribute.inherited:
1043
+ generics_with_attribute = []
1044
+ for generic_name in node_schema.inherit_from:
1045
+ generic_schema = self.get_generic(name=generic_name, duplicate=False)
1046
+ if attribute.name in generic_schema.attribute_names:
1047
+ generic_attribute = generic_schema.get_attribute(name=attribute.name)
1048
+ generics_with_attribute.append(generic_schema)
1049
+ if isinstance(generic_attribute.parameters, NumberPoolParameters):
1050
+ number_pool_parameters.number_pool_id = generic_attribute.parameters.number_pool_id
1051
+
1052
+ if len(generics_with_attribute) > 1:
1053
+ raise ValidationError(
1054
+ f"{node_schema.kind}.{attribute.name} is a NumberPool inherited from more than one generic"
1055
+ )
1056
+
1057
+ else:
1058
+ number_pool_parameters.number_pool_id = str(uuid4())
997
1059
 
998
1060
  def validate_computed_attributes(self) -> None:
999
1061
  self.computed_attributes = ComputedAttributes()
@@ -1960,7 +2022,11 @@ class SchemaBranch:
1960
2022
  )
1961
2023
 
1962
2024
  parent_hfid = f"{relationship.name}__template_name__value"
1963
- if relationship.kind == RelationshipKind.PARENT and parent_hfid not in template_schema.human_friendly_id:
2025
+ if (
2026
+ not isinstance(template_schema, GenericSchema)
2027
+ and relationship.kind == RelationshipKind.PARENT
2028
+ and parent_hfid not in template_schema.human_friendly_id
2029
+ ):
1964
2030
  template_schema.human_friendly_id = [parent_hfid] + template_schema.human_friendly_id
1965
2031
  template_schema.uniqueness_constraints[0].append(relationship.name)
1966
2032
 
@@ -1996,7 +2062,6 @@ class SchemaBranch:
1996
2062
  include_in_menu=False,
1997
2063
  display_labels=["template_name__value"],
1998
2064
  human_friendly_id=["template_name__value"],
1999
- uniqueness_constraints=[["template_name__value"]],
2000
2065
  attributes=[template_name_attr],
2001
2066
  )
2002
2067
 
@@ -2015,7 +2080,6 @@ class SchemaBranch:
2015
2080
  human_friendly_id=["template_name__value"],
2016
2081
  uniqueness_constraints=[["template_name__value"]],
2017
2082
  inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind],
2018
- default_filter="template_name__value",
2019
2083
  attributes=[template_name_attr],
2020
2084
  relationships=[
2021
2085
  RelationshipSchema(
@@ -1,7 +1,10 @@
1
+ from infrahub.core.validators.attribute.min_max import AttributeNumberChecker
2
+
1
3
  from .attribute.choices import AttributeChoicesChecker
2
4
  from .attribute.enum import AttributeEnumChecker
3
5
  from .attribute.kind import AttributeKindChecker
4
6
  from .attribute.length import AttributeLengthChecker
7
+ from .attribute.number_pool import AttributeNumberPoolChecker
5
8
  from .attribute.optional import AttributeOptionalChecker
6
9
  from .attribute.regex import AttributeRegexChecker
7
10
  from .attribute.unique import AttributeUniquenessChecker
@@ -26,6 +29,11 @@ CONSTRAINT_VALIDATOR_MAP: dict[str, type[ConstraintCheckerInterface] | None] = {
26
29
  "attribute.max_length.update": AttributeLengthChecker,
27
30
  ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MIN_LENGTH_UPDATE.value: AttributeLengthChecker,
28
31
  ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MAX_LENGTH_UPDATE.value: AttributeLengthChecker,
32
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MIN_VALUE_UPDATE.value: AttributeNumberChecker,
33
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MAX_VALUE_UPDATE.value: AttributeNumberChecker,
34
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_EXCLUDED_VALUES_UPDATE.value: AttributeNumberChecker,
35
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_START_RANGE_UPDATE: AttributeNumberPoolChecker,
36
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_END_RANGE_UPDATE: AttributeNumberPoolChecker,
29
37
  "attribute.unique.update": AttributeUniquenessChecker,
30
38
  "attribute.optional.update": AttributeOptionalChecker,
31
39
  "attribute.choices.update": AttributeChoicesChecker,
@@ -42,7 +42,6 @@ class AttributeChoicesUpdateValidatorQuery(AttributeSchemaValidatorQuery):
42
42
  LIMIT 1
43
43
  }
44
44
  WITH full_path, node, attribute_value, value_relationship
45
- WITH full_path, node, attribute_value, value_relationship
46
45
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
47
46
  AND attribute_value IS NOT NULL
48
47
  AND attribute_value <> $null_value
@@ -41,7 +41,6 @@ class AttributeEnumUpdateValidatorQuery(AttributeSchemaValidatorQuery):
41
41
  LIMIT 1
42
42
  }
43
43
  WITH full_path, node, attribute_value, value_relationship
44
- WITH full_path, node, attribute_value, value_relationship
45
44
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
46
45
  AND attribute_value IS NOT NULL
47
46
  AND attribute_value <> $null_value
@@ -48,7 +48,6 @@ class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
48
48
  LIMIT 1
49
49
  }
50
50
  WITH full_path, node, attribute_value, value_relationship
51
- WITH full_path, node, attribute_value, value_relationship
52
51
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
53
52
  AND attribute_value IS NOT NULL
54
53
  AND attribute_value <> $null_value
@@ -40,7 +40,6 @@ class AttributeLengthUpdateValidatorQuery(AttributeSchemaValidatorQuery):
40
40
  LIMIT 1
41
41
  }
42
42
  WITH full_path, node, attribute_value, value_relationship
43
- WITH full_path, node, attribute_value, value_relationship
44
43
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
45
44
  AND (
46
45
  (toInteger($min_length) IS NOT NULL AND size(attribute_value) < toInteger($min_length))
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from infrahub.core.constants import PathType
6
+ from infrahub.core.path import DataPath, GroupedDataPaths
7
+ from infrahub.core.schema.attribute_parameters import NumberAttributeParameters
8
+ from infrahub.core.validators.enum import ConstraintIdentifier
9
+
10
+ from ..interface import ConstraintCheckerInterface
11
+ from ..shared import AttributeSchemaValidatorQuery
12
+
13
+ if TYPE_CHECKING:
14
+ from infrahub.core.branch import Branch
15
+ from infrahub.database import InfrahubDatabase
16
+
17
+ from ..model import SchemaConstraintValidatorRequest
18
+
19
+
20
+ class AttributeNumberUpdateValidatorQuery(AttributeSchemaValidatorQuery):
21
+ name: str = "attribute_constraints_number_validator"
22
+
23
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
24
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
25
+ self.params.update(branch_params)
26
+
27
+ if not isinstance(self.attribute_schema.parameters, NumberAttributeParameters):
28
+ raise ValueError("attribute parameters are not a NumberAttributeParameters")
29
+
30
+ self.params["attr_name"] = self.attribute_schema.name
31
+ self.params["min_value"] = self.attribute_schema.parameters.min_value
32
+ self.params["max_value"] = self.attribute_schema.parameters.max_value
33
+ self.params["excluded_values"] = self.attribute_schema.parameters.get_excluded_single_values()
34
+ self.params["excluded_ranges"] = self.attribute_schema.parameters.get_excluded_ranges()
35
+
36
+ query = """
37
+ MATCH (n:%(node_kind)s)
38
+ CALL (n) {
39
+ MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
40
+ WHERE all(
41
+ r in relationships(path)
42
+ WHERE %(branch_filter)s
43
+ )
44
+ RETURN path as full_path, n as node, rv as value_relationship, av.value as attribute_value
45
+ ORDER BY rv.branch_level DESC, ra.branch_level DESC, rr.branch_level DESC, rv.from DESC, ra.from DESC, rr.from DESC
46
+ LIMIT 1
47
+ }
48
+ WITH full_path, node, attribute_value, value_relationship
49
+ WHERE all(r in relationships(full_path) WHERE r.status = "active")
50
+ AND (
51
+ (toInteger($min_value) IS NOT NULL AND attribute_value < toInteger($min_value))
52
+ OR (toInteger($max_value) IS NOT NULL AND attribute_value > toInteger($max_value))
53
+ OR (size($excluded_values) > 0 AND attribute_value IN $excluded_values)
54
+ OR (size($excluded_ranges) > 0 AND any(range in $excluded_ranges WHERE attribute_value >= range[0] AND attribute_value <= range[1]))
55
+ )
56
+ """ % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}
57
+
58
+ self.add_to_query(query)
59
+ self.return_labels = ["node.uuid", "value_relationship", "attribute_value"]
60
+
61
+ async def get_paths(self) -> GroupedDataPaths:
62
+ grouped_data_paths = GroupedDataPaths()
63
+ for result in self.results:
64
+ grouped_data_paths.add_data_path(
65
+ DataPath(
66
+ branch=str(result.get("value_relationship").get("branch")),
67
+ path_type=PathType.ATTRIBUTE,
68
+ node_id=str(result.get("node.uuid")),
69
+ field_name=self.attribute_schema.name,
70
+ kind=self.node_schema.kind,
71
+ value=result.get("attribute_value"),
72
+ ),
73
+ )
74
+
75
+ return grouped_data_paths
76
+
77
+
78
+ class AttributeNumberChecker(ConstraintCheckerInterface):
79
+ query_classes = [AttributeNumberUpdateValidatorQuery]
80
+
81
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
82
+ self.db = db
83
+ self.branch = branch
84
+
85
+ @property
86
+ def name(self) -> str:
87
+ return "attribute.number.update"
88
+
89
+ def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
90
+ return request.constraint_name in (
91
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MIN_VALUE_UPDATE.value,
92
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MAX_VALUE_UPDATE.value,
93
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_EXCLUDED_VALUES_UPDATE.value,
94
+ )
95
+
96
+ async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
97
+ grouped_data_paths_list: list[GroupedDataPaths] = []
98
+ if not request.schema_path.field_name:
99
+ raise ValueError("field_name is not defined")
100
+ attribute_schema = request.node_schema.get_attribute(name=request.schema_path.field_name)
101
+ if not isinstance(attribute_schema.parameters, NumberAttributeParameters):
102
+ raise ValueError("attribute parameters are not a NumberAttributeParameters")
103
+
104
+ if (
105
+ attribute_schema.parameters.min_value is None
106
+ and attribute_schema.parameters.max_value is None
107
+ and attribute_schema.parameters.excluded_values is None
108
+ ):
109
+ return grouped_data_paths_list
110
+
111
+ for query_class in self.query_classes:
112
+ # TODO add exception handling
113
+ query = await query_class.init(
114
+ db=self.db, branch=self.branch, node_schema=request.node_schema, schema_path=request.schema_path
115
+ )
116
+ await query.execute(db=self.db)
117
+ grouped_data_paths_list.append(await query.get_paths())
118
+ return grouped_data_paths_list
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from infrahub.core.constants import PathType
6
+ from infrahub.core.path import DataPath, GroupedDataPaths
7
+ from infrahub.core.schema.attribute_parameters import NumberPoolParameters
8
+ from infrahub.core.validators.enum import ConstraintIdentifier
9
+
10
+ from ..interface import ConstraintCheckerInterface
11
+ from ..shared import AttributeSchemaValidatorQuery
12
+
13
+ if TYPE_CHECKING:
14
+ from infrahub.core.branch import Branch
15
+ from infrahub.database import InfrahubDatabase
16
+
17
+ from ..model import SchemaConstraintValidatorRequest
18
+
19
+
20
+ class AttributeNumberPoolUpdateValidatorQuery(AttributeSchemaValidatorQuery):
21
+ name: str = "attribute_constraints_numberpool_validator"
22
+
23
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
24
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
25
+ self.params.update(branch_params)
26
+
27
+ if not isinstance(self.attribute_schema.parameters, NumberPoolParameters):
28
+ raise ValueError("attribute parameters are not a NumberPoolParameters")
29
+
30
+ self.params["attr_name"] = self.attribute_schema.name
31
+ self.params["start_range"] = self.attribute_schema.parameters.start_range
32
+ self.params["end_range"] = self.attribute_schema.parameters.end_range
33
+
34
+ query = """
35
+ MATCH (n:%(node_kind)s)
36
+ CALL (n) {
37
+ MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
38
+ WHERE all(
39
+ r in relationships(path)
40
+ WHERE %(branch_filter)s
41
+ )
42
+ RETURN path as full_path, n as node, rv as value_relationship, av.value as attribute_value
43
+ ORDER BY rv.branch_level DESC, ra.branch_level DESC, rr.branch_level DESC, rv.from DESC, ra.from DESC, rr.from DESC
44
+ LIMIT 1
45
+ }
46
+ WITH full_path, node, attribute_value, value_relationship
47
+ WHERE all(r in relationships(full_path) WHERE r.status = "active")
48
+ AND (
49
+ (toInteger($start_range) IS NOT NULL AND attribute_value < toInteger($start_range))
50
+ OR (toInteger($end_range) IS NOT NULL AND attribute_value > toInteger($end_range))
51
+ )
52
+ """ % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}
53
+
54
+ self.add_to_query(query)
55
+ self.return_labels = ["node.uuid", "value_relationship", "attribute_value"]
56
+
57
+ async def get_paths(self) -> GroupedDataPaths:
58
+ grouped_data_paths = GroupedDataPaths()
59
+ for result in self.results:
60
+ grouped_data_paths.add_data_path(
61
+ DataPath(
62
+ branch=str(result.get("value_relationship").get("branch")),
63
+ path_type=PathType.ATTRIBUTE,
64
+ node_id=str(result.get("node.uuid")),
65
+ field_name=self.attribute_schema.name,
66
+ kind=self.node_schema.kind,
67
+ value=result.get("attribute_value"),
68
+ ),
69
+ )
70
+
71
+ return grouped_data_paths
72
+
73
+
74
+ class AttributeNumberPoolChecker(ConstraintCheckerInterface):
75
+ query_classes = [AttributeNumberPoolUpdateValidatorQuery]
76
+
77
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
78
+ self.db = db
79
+ self.branch = branch
80
+
81
+ @property
82
+ def name(self) -> str:
83
+ return "attribute.number.update"
84
+
85
+ def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
86
+ return request.constraint_name in (
87
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_START_RANGE_UPDATE.value,
88
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_END_RANGE_UPDATE.value,
89
+ )
90
+
91
+ async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
92
+ grouped_data_paths_list: list[GroupedDataPaths] = []
93
+ if not request.schema_path.field_name:
94
+ raise ValueError("field_name is not defined")
95
+ attribute_schema = request.node_schema.get_attribute(name=request.schema_path.field_name)
96
+ if not isinstance(attribute_schema.parameters, NumberPoolParameters):
97
+ raise ValueError("attribute parameters are not a NumberPoolParameters")
98
+
99
+ for query_class in self.query_classes:
100
+ # TODO add exception handling
101
+ query = await query_class.init(
102
+ db=self.db, branch=self.branch, node_schema=request.node_schema, schema_path=request.schema_path
103
+ )
104
+ await query.execute(db=self.db)
105
+ grouped_data_paths_list.append(await query.get_paths())
106
+ return grouped_data_paths_list
@@ -38,7 +38,6 @@ class AttributeOptionalUpdateValidatorQuery(AttributeSchemaValidatorQuery):
38
38
  LIMIT 1
39
39
  }
40
40
  WITH full_path, node, attribute_value, value_relationship
41
- WITH full_path, node, attribute_value, value_relationship
42
41
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
43
42
  AND (attribute_value IS NULL OR attribute_value = $null_value)
44
43
  """ % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}
@@ -85,7 +84,6 @@ class AttributeOptionalChecker(ConstraintCheckerInterface):
85
84
  return grouped_data_paths_list
86
85
 
87
86
  for query_class in self.query_classes:
88
- # TODO add exception handling
89
87
  query = await query_class.init(
90
88
  db=self.db, branch=self.branch, node_schema=request.node_schema, schema_path=request.schema_path
91
89
  )
@@ -39,7 +39,6 @@ class AttributeRegexUpdateValidatorQuery(AttributeSchemaValidatorQuery):
39
39
  LIMIT 1
40
40
  }
41
41
  WITH full_path, node, attribute_value, value_relationship
42
- WITH full_path, node, attribute_value, value_relationship
43
42
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
44
43
  AND attribute_value <> $null_value
45
44
  AND NOT attribute_value =~ $attr_value_regex
@@ -5,3 +5,8 @@ class ConstraintIdentifier(str, Enum):
5
5
  ATTRIBUTE_PARAMETERS_REGEX_UPDATE = "attribute.parameters.regex.update"
6
6
  ATTRIBUTE_PARAMETERS_MIN_LENGTH_UPDATE = "attribute.parameters.min_length.update"
7
7
  ATTRIBUTE_PARAMETERS_MAX_LENGTH_UPDATE = "attribute.parameters.max_length.update"
8
+ ATTRIBUTE_PARAMETERS_MIN_VALUE_UPDATE = "attribute.parameters.min_value.update"
9
+ ATTRIBUTE_PARAMETERS_MAX_VALUE_UPDATE = "attribute.parameters.max_value.update"
10
+ ATTRIBUTE_PARAMETERS_EXCLUDED_VALUES_UPDATE = "attribute.parameters.excluded_values.update"
11
+ ATTRIBUTE_PARAMETERS_END_RANGE_UPDATE = "attribute.parameters.end_range.update"
12
+ ATTRIBUTE_PARAMETERS_START_RANGE_UPDATE = "attribute.parameters.start_range.update"
@@ -9,7 +9,7 @@ from prefect.logging import get_run_logger
9
9
 
10
10
  from infrahub.core.branch import Branch # noqa: TC001
11
11
  from infrahub.core.path import SchemaPath # noqa: TC001
12
- from infrahub.core.schema import GenericSchema, NodeSchema # noqa: TC001
12
+ from infrahub.core.schema import GenericSchema, NodeSchema
13
13
  from infrahub.core.validators.aggregated_checker import AggregatedConstraintChecker
14
14
  from infrahub.core.validators.model import (
15
15
  SchemaConstraintValidatorRequest,
@@ -39,7 +39,7 @@ if TYPE_CHECKING:
39
39
  from types import TracebackType
40
40
 
41
41
  from infrahub.core.branch import Branch
42
- from infrahub.core.schema import MainSchemaTypes, NodeSchema
42
+ from infrahub.core.schema import GenericSchema, MainSchemaTypes, NodeSchema
43
43
  from infrahub.core.schema.schema_branch import SchemaBranch
44
44
 
45
45
  validated_database = {}
@@ -91,6 +91,15 @@ class DatabaseSchemaManager:
91
91
 
92
92
  raise ValueError("The selected node is not of type NodeSchema")
93
93
 
94
+ def get_generic_schema(
95
+ self, name: str, branch: Branch | str | None = None, duplicate: bool = True
96
+ ) -> GenericSchema:
97
+ schema = self.get(name=name, branch=branch, duplicate=duplicate)
98
+ if schema.is_generic_schema:
99
+ return schema
100
+
101
+ raise ValueError("The selected node is not of type GenericSchema")
102
+
94
103
  def set(self, name: str, schema: MainSchemaTypes, branch: str | None = None) -> int:
95
104
  branch_name = get_branch_name(branch=branch)
96
105
  if branch_name not in self._db._schemas:
@@ -284,7 +293,7 @@ class InfrahubDatabase:
284
293
  exc_type: type[BaseException] | None,
285
294
  exc_value: BaseException | None,
286
295
  traceback: TracebackType | None,
287
- ):
296
+ ) -> None:
288
297
  if self._mode == InfrahubDatabaseMode.SESSION:
289
298
  return await self._session.close()
290
299
 
@@ -330,7 +339,7 @@ class InfrahubDatabase:
330
339
  CONNECTION_POOL_USAGE.labels(self._driver._pool.address).set(float(connpool_usage))
331
340
 
332
341
  if config.SETTINGS.database.max_concurrent_queries:
333
- while connpool_usage > config.SETTINGS.database.max_concurrent_queries: # noqa: ASYNC110
342
+ while connpool_usage > config.SETTINGS.database.max_concurrent_queries:
334
343
  await asyncio.sleep(config.SETTINGS.database.max_concurrent_queries_delay)
335
344
  connpool_usage = self._driver._pool.in_use_connection_count(self._driver._pool.address)
336
345
 
@@ -487,7 +496,10 @@ async def get_db(retry: int = 0) -> AsyncDriver:
487
496
  auth=(config.SETTINGS.database.username, config.SETTINGS.database.password),
488
497
  encrypted=config.SETTINGS.database.tls_enabled,
489
498
  trusted_certificates=trusted_certificates,
490
- notifications_disabled_categories=[NotificationDisabledCategory.UNRECOGNIZED],
499
+ notifications_disabled_categories=[
500
+ NotificationDisabledCategory.UNRECOGNIZED,
501
+ NotificationDisabledCategory.DEPRECATION, # TODO: Remove me with 1.3
502
+ ],
491
503
  notifications_min_severity=NotificationMinimumSeverity.WARNING,
492
504
  )
493
505
 
@@ -0,0 +1,100 @@
1
+ from infrahub.database import InfrahubDatabase
2
+
3
+
4
+ async def verify_no_duplicate_relationships(db: InfrahubDatabase) -> None:
5
+ """
6
+ Verify that no duplicate active relationships exist at the database level
7
+ A duplicate is defined as
8
+ - connecting the same two nodes
9
+ - having the same identifier
10
+ - having the same direction (inbound, outbound, bidirectional)
11
+ - having the same branch
12
+ A more thorough check that no duplicates exist at any point in time is possible, but more complex
13
+ """
14
+ query = """
15
+ MATCH (a:Node)-[e1:IS_RELATED {status: "active"}]-(rel:Relationship)-[e2:IS_RELATED {branch: e1.branch, status: "active"}]-(b:Node)
16
+ WHERE a.uuid <> b.uuid
17
+ AND e1.to IS NULL
18
+ AND e2.to IS NULL
19
+ WITH a, rel.name AS rel_name, b, e1.branch AS branch, CASE
20
+ WHEN startNode(e1) = a AND startNode(e2) = rel THEN "out"
21
+ WHEN startNode(e1) = rel AND startNode(e2) = b THEN "in"
22
+ ELSE "bidir"
23
+ END AS direction, COUNT(*) AS num_duplicates
24
+ WHERE num_duplicates > 1
25
+ RETURN a.uuid AS node_id1, b.uuid AS node_id2, rel_name, branch, direction, num_duplicates
26
+ """
27
+ results = await db.execute_query(query=query)
28
+ for result in results:
29
+ node_id1 = result.get("node_id1")
30
+ node_id2 = result.get("node_id2")
31
+ rel_name = result.get("rel_name")
32
+ branch = result.get("branch")
33
+ direction = result.get("direction")
34
+ num_duplicates = result.get("num_duplicates")
35
+ raise ValueError(
36
+ f"{num_duplicates} duplicate relationships ({branch=},{direction=}) between nodes '{node_id1}' and '{node_id2}'"
37
+ f" with relationship name '{rel_name}'"
38
+ )
39
+
40
+
41
+ async def verify_no_edges_added_after_node_delete(db: InfrahubDatabase) -> None:
42
+ """
43
+ Verify that no edges are added to a Node after it is deleted on a given branch
44
+ """
45
+ query = """
46
+ // ------------
47
+ // find deleted nodes
48
+ // ------------
49
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
50
+ WHERE e.status = "deleted" OR e.to IS NOT NULL
51
+ WITH DISTINCT n, e.branch AS delete_branch, e.branch_level AS delete_branch_level, CASE
52
+ WHEN e.status = "deleted" THEN e.from
53
+ ELSE e.to
54
+ END AS delete_time
55
+ // ------------
56
+ // find the edges added to the deleted node after the delete time
57
+ // ------------
58
+ MATCH (n)-[added_e]-(peer)
59
+ WHERE added_e.from > delete_time
60
+ AND type(added_e) <> "IS_PART_OF"
61
+ // if the node was deleted on a branch (delete_branch_level > 1), and then updated on main/global (added_e.branch_level = 1), we can ignore it
62
+ AND added_e.branch_level >= delete_branch_level
63
+ AND (added_e.branch = delete_branch OR delete_branch_level = 1)
64
+ WITH DISTINCT n, delete_branch, delete_time, added_e, peer AS added_peer
65
+ // ------------
66
+ // get the branched_from for the branch on which the node was deleted
67
+ // ------------
68
+ CALL (added_e) {
69
+ MATCH (b:Branch {name: added_e.branch})
70
+ RETURN b.branched_from AS added_e_branched_from
71
+ }
72
+ // ------------
73
+ // account for the following situations, given that the edge update time is after the node delete time
74
+ // - deleted on main/global, updated on branch
75
+ // - illegal if the delete is before branch.branched_from
76
+ // - deleted on branch, updated on branch
77
+ // - illegal
78
+ // ------------
79
+ WITH n, delete_branch, delete_time, added_e, added_peer
80
+ WHERE delete_branch = added_e.branch
81
+ OR delete_time < added_e_branched_from
82
+ RETURN n.uuid AS n_uuid, delete_branch, delete_time, added_e, added_peer
83
+ """
84
+ results = await db.execute_query(query=query)
85
+ error_messages = []
86
+ for result in results:
87
+ n_uuid = result.get("n_uuid")
88
+ delete_branch = result.get("delete_branch")
89
+ delete_time = result.get("delete_time")
90
+ added_e = result.get("added_e")
91
+ added_e_branch = added_e.get("branch")
92
+ added_e_from = added_e.get("from")
93
+ added_peer = result.get("added_peer")
94
+ message = (
95
+ f"Node {n_uuid} was deleted on {delete_branch} at {delete_time} but has an {added_e.type} edge added on"
96
+ f" branch {added_e_branch} at {added_e_from} to {added_peer.element_id}"
97
+ )
98
+ error_messages.append(message)
99
+ if error_messages:
100
+ raise ValueError(error_messages)
@@ -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_relatives import RelationshipPeerRelativesConstraintDependency
7
8
  from ..relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
8
9
 
9
10
 
@@ -18,5 +19,6 @@ class NodeConstraintRunnerDependency(DependencyBuilder[NodeConstraintRunner]):
18
19
  RelationshipPeerKindConstraintDependency.build(context=context),
19
20
  RelationshipCountConstraintDependency.build(context=context),
20
21
  RelationshipProfilesKindConstraintDependency.build(context=context),
22
+ RelationshipPeerRelativesConstraintDependency.build(context=context),
21
23
  ],
22
24
  )
@@ -0,0 +1,8 @@
1
+ from infrahub.core.relationship.constraints.peer_relatives import RelationshipPeerRelativesConstraint
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class RelationshipPeerRelativesConstraintDependency(DependencyBuilder[RelationshipPeerRelativesConstraint]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> RelationshipPeerRelativesConstraint:
8
+ return RelationshipPeerRelativesConstraint(db=context.db, branch=context.branch)
@@ -6,5 +6,5 @@ from .parent_node_adder import DiffParentNodeAdderDependency
6
6
 
7
7
  class DiffDeserializerDependency(DependencyBuilder[EnrichedDiffDeserializer]):
8
8
  @classmethod
9
- def build(cls, context: DependencyBuilderContext) -> EnrichedDiffDeserializer: # noqa: ARG003
9
+ def build(cls, context: DependencyBuilderContext) -> EnrichedDiffDeserializer:
10
10
  return EnrichedDiffDeserializer(parent_adder=DiffParentNodeAdderDependency.build(context=context))