infrahub-server 1.2.10__py3-none-any.whl → 1.3.0a0__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 (134) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/config.py +9 -0
  10. infrahub/core/account.py +24 -47
  11. infrahub/core/attribute.py +10 -12
  12. infrahub/core/constants/infrahubkind.py +8 -0
  13. infrahub/core/constraint/node/runner.py +1 -1
  14. infrahub/core/convert_object_type/__init__.py +0 -0
  15. infrahub/core/convert_object_type/conversion.py +122 -0
  16. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  17. infrahub/core/diff/query/all_conflicts.py +1 -5
  18. infrahub/core/diff/query/artifact.py +10 -20
  19. infrahub/core/diff/query/diff_get.py +3 -6
  20. infrahub/core/diff/query/field_summary.py +2 -4
  21. infrahub/core/diff/query/merge.py +70 -123
  22. infrahub/core/diff/query/save.py +20 -32
  23. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  24. infrahub/core/diff/query_parser.py +5 -1
  25. infrahub/core/diff/tasks.py +3 -3
  26. infrahub/core/manager.py +14 -11
  27. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  28. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  29. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  30. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  31. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  32. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  33. infrahub/core/migrations/query/attribute_add.py +1 -2
  34. infrahub/core/migrations/query/attribute_rename.py +3 -6
  35. infrahub/core/migrations/query/delete_element_in_schema.py +3 -6
  36. infrahub/core/migrations/query/node_duplicate.py +3 -6
  37. infrahub/core/migrations/query/relationship_duplicate.py +3 -6
  38. infrahub/core/migrations/schema/node_attribute_remove.py +3 -6
  39. infrahub/core/migrations/schema/node_remove.py +3 -6
  40. infrahub/core/models.py +29 -2
  41. infrahub/core/node/__init__.py +18 -4
  42. infrahub/core/node/create.py +211 -0
  43. infrahub/core/protocols.py +51 -0
  44. infrahub/core/protocols_base.py +3 -0
  45. infrahub/core/query/__init__.py +2 -2
  46. infrahub/core/query/diff.py +26 -32
  47. infrahub/core/query/ipam.py +10 -20
  48. infrahub/core/query/node.py +28 -46
  49. infrahub/core/query/relationship.py +51 -28
  50. infrahub/core/query/resource_manager.py +1 -2
  51. infrahub/core/query/subquery.py +2 -4
  52. infrahub/core/relationship/model.py +3 -0
  53. infrahub/core/schema/__init__.py +2 -1
  54. infrahub/core/schema/attribute_parameters.py +36 -0
  55. infrahub/core/schema/attribute_schema.py +83 -8
  56. infrahub/core/schema/basenode_schema.py +25 -1
  57. infrahub/core/schema/definitions/core/__init__.py +21 -0
  58. infrahub/core/schema/definitions/internal.py +13 -3
  59. infrahub/core/schema/generated/attribute_schema.py +9 -3
  60. infrahub/core/schema/schema_branch.py +12 -7
  61. infrahub/core/validators/__init__.py +5 -1
  62. infrahub/core/validators/attribute/choices.py +1 -2
  63. infrahub/core/validators/attribute/enum.py +1 -2
  64. infrahub/core/validators/attribute/kind.py +1 -2
  65. infrahub/core/validators/attribute/length.py +13 -6
  66. infrahub/core/validators/attribute/optional.py +1 -2
  67. infrahub/core/validators/attribute/regex.py +5 -5
  68. infrahub/core/validators/attribute/unique.py +1 -3
  69. infrahub/core/validators/determiner.py +18 -2
  70. infrahub/core/validators/enum.py +7 -0
  71. infrahub/core/validators/node/hierarchy.py +3 -6
  72. infrahub/core/validators/query.py +1 -3
  73. infrahub/core/validators/relationship/count.py +6 -12
  74. infrahub/core/validators/relationship/optional.py +2 -4
  75. infrahub/core/validators/relationship/peer.py +3 -8
  76. infrahub/core/validators/tasks.py +1 -1
  77. infrahub/core/validators/uniqueness/query.py +5 -9
  78. infrahub/database/__init__.py +1 -3
  79. infrahub/events/group_action.py +1 -0
  80. infrahub/graphql/analyzer.py +139 -18
  81. infrahub/graphql/app.py +1 -1
  82. infrahub/graphql/loaders/node.py +1 -1
  83. infrahub/graphql/loaders/peers.py +1 -1
  84. infrahub/graphql/manager.py +4 -0
  85. infrahub/graphql/mutations/action.py +164 -0
  86. infrahub/graphql/mutations/convert_object_type.py +62 -0
  87. infrahub/graphql/mutations/main.py +24 -175
  88. infrahub/graphql/mutations/proposed_change.py +21 -18
  89. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  90. infrahub/graphql/queries/relationship.py +1 -1
  91. infrahub/graphql/resolvers/many_relationship.py +4 -4
  92. infrahub/graphql/resolvers/resolver.py +4 -4
  93. infrahub/graphql/resolvers/single_relationship.py +2 -2
  94. infrahub/graphql/schema.py +6 -0
  95. infrahub/graphql/subscription/graphql_query.py +2 -2
  96. infrahub/graphql/types/branch.py +1 -1
  97. infrahub/menu/menu.py +31 -0
  98. infrahub/message_bus/messages/__init__.py +0 -10
  99. infrahub/message_bus/operations/__init__.py +0 -8
  100. infrahub/message_bus/operations/refresh/registry.py +1 -1
  101. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  102. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  103. infrahub/prefect_server/models.py +1 -19
  104. infrahub/proposed_change/models.py +68 -3
  105. infrahub/proposed_change/tasks.py +907 -30
  106. infrahub/task_manager/models.py +10 -6
  107. infrahub/telemetry/database.py +1 -1
  108. infrahub/telemetry/tasks.py +1 -1
  109. infrahub/trigger/catalogue.py +2 -0
  110. infrahub/trigger/models.py +18 -2
  111. infrahub/trigger/tasks.py +3 -1
  112. infrahub/workflows/catalogue.py +76 -0
  113. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/METADATA +2 -2
  114. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/RECORD +121 -118
  115. infrahub_testcontainers/container.py +0 -1
  116. infrahub_testcontainers/docker-compose.test.yml +1 -1
  117. infrahub_testcontainers/helpers.py +8 -2
  118. infrahub/message_bus/messages/check_generator_run.py +0 -26
  119. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  120. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  121. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  122. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  123. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  124. infrahub/message_bus/operations/check/__init__.py +0 -3
  125. infrahub/message_bus/operations/check/generator.py +0 -156
  126. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  127. infrahub/message_bus/operations/finalize/validator.py +0 -133
  128. infrahub/message_bus/operations/requests/__init__.py +0 -9
  129. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  130. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  131. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  132. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/LICENSE.txt +0 -0
  133. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/WHEEL +0 -0
  134. {infrahub_server-1.2.10.dist-info → infrahub_server-1.3.0a0.dist-info}/entry_points.txt +0 -0
@@ -220,8 +220,7 @@ class NumberPoolGetUsed(Query):
220
220
 
221
221
  query = """
222
222
  MATCH (pool:%(number_pool)s { uuid: $pool_id })
223
- CALL {
224
- WITH pool
223
+ CALL (pool) {
225
224
  MATCH (pool)-[res:IS_RESERVED]->(av:AttributeValue)<-[hv:HAS_VALUE]-(attr:Attribute)
226
225
  WHERE
227
226
  attr.name = $attribute_name
@@ -61,7 +61,7 @@ async def build_subquery_filter(
61
61
  where_str = " AND ".join(field_where)
62
62
  branch_level_str = "reduce(br_lvl = 0, r in relationships(path) | br_lvl + r.branch_level)"
63
63
  froms_str = db.render_list_comprehension(items="relationships(path)", item_name="from")
64
- to_return = f"{node_alias} AS {prefix}"
64
+ to_return = f"{prefix}"
65
65
  with_extra = ""
66
66
  final_with_extra = ""
67
67
  is_isnull = filter_name == "isnull"
@@ -82,7 +82,6 @@ async def build_subquery_filter(
82
82
  elif field is not None and field.is_attribute:
83
83
  is_active_filter = "(latest_node_details[2]).value = 'NULL'"
84
84
  query = f"""
85
- WITH {node_alias}
86
85
  {match} path = {filter_str}
87
86
  WHERE {where_str}
88
87
  WITH
@@ -94,7 +93,7 @@ async def build_subquery_filter(
94
93
  ORDER BY branch_level DESC, froms[-1] DESC, froms[-2] DESC
95
94
  WITH head(collect([is_active, {node_alias}{with_extra}])) AS latest_node_details
96
95
  WHERE {is_active_filter}
97
- WITH latest_node_details[1] AS {node_alias}{final_with_extra}
96
+ WITH latest_node_details[1] AS {prefix}{final_with_extra}
98
97
  RETURN {to_return}
99
98
  """
100
99
  return query, params, prefix
@@ -174,7 +173,6 @@ async def build_subquery_order(
174
173
  to_return_str_parts.append(f"CASE WHEN is_active = TRUE THEN {expression} ELSE NULL END AS {alias}")
175
174
  to_return_str = ", ".join(to_return_str_parts)
176
175
  query = f"""
177
- WITH {node_alias}
178
176
  OPTIONAL MATCH path = {filter_str}
179
177
  WHERE {where_str}
180
178
  WITH {with_str_to_alias}
@@ -789,6 +789,9 @@ class RelationshipManager:
789
789
 
790
790
  return len(self._relationships)
791
791
 
792
+ def validate(self) -> None:
793
+ self._relationships.validate()
794
+
792
795
  @overload
793
796
  async def get_peer(
794
797
  self,
@@ -21,7 +21,8 @@ from .profile_schema import ProfileSchema
21
21
  from .relationship_schema import RelationshipSchema
22
22
  from .template_schema import TemplateSchema
23
23
 
24
- MainSchemaTypes: TypeAlias = NodeSchema | GenericSchema | ProfileSchema | TemplateSchema
24
+ NonGenericSchemaTypes: TypeAlias = NodeSchema | ProfileSchema | TemplateSchema
25
+ MainSchemaTypes: TypeAlias = NonGenericSchemaTypes | GenericSchema
25
26
 
26
27
 
27
28
  # -----------------------------------------------------
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+
5
+ from infrahub.core.constants.schema import UpdateSupport
6
+ from infrahub.core.models import HashableModel
7
+
8
+
9
+ def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParameters]:
10
+ return {
11
+ "Text": TextAttributeParameters,
12
+ "TextArea": TextAttributeParameters,
13
+ }.get(kind, AttributeParameters)
14
+
15
+
16
+ class AttributeParameters(HashableModel):
17
+ class Config:
18
+ extra = "forbid"
19
+
20
+
21
+ class TextAttributeParameters(AttributeParameters):
22
+ regex: str | None = Field(
23
+ default=None,
24
+ description="Regular expression that attribute value must match if defined",
25
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
26
+ )
27
+ min_length: int | None = Field(
28
+ default=None,
29
+ description="Set a minimum number of characters allowed.",
30
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
31
+ )
32
+ max_length: int | None = Field(
33
+ default=None,
34
+ description="Set a maximum number of characters allowed.",
35
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
36
+ )
@@ -2,15 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import enum
4
4
  from enum import Enum
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, Self
6
6
 
7
- from pydantic import field_validator, model_validator
7
+ from pydantic import Field, ValidationInfo, field_validator, model_validator
8
8
 
9
9
  from infrahub import config
10
+ from infrahub.core.constants.schema import UpdateSupport
10
11
  from infrahub.core.enums import generate_python_enum
11
12
  from infrahub.core.query.attribute import default_attribute_query_filter
12
13
  from infrahub.types import ATTRIBUTE_KIND_LABELS, ATTRIBUTE_TYPES
13
14
 
15
+ from .attribute_parameters import AttributeParameters, TextAttributeParameters, get_attribute_parameters_class_for_kind
14
16
  from .generated.attribute_schema import GeneratedAttributeSchema
15
17
 
16
18
  if TYPE_CHECKING:
@@ -21,6 +23,14 @@ if TYPE_CHECKING:
21
23
  from infrahub.database import InfrahubDatabase
22
24
 
23
25
 
26
+ def get_attribute_schema_class_for_kind(kind: str) -> type[AttributeSchema]:
27
+ attribute_schema_class_by_kind: dict[str, type[AttributeSchema]] = {
28
+ "Text": TextAttributeSchema,
29
+ "TextArea": TextAttributeSchema,
30
+ }
31
+ return attribute_schema_class_by_kind.get(kind, AttributeSchema)
32
+
33
+
24
34
  class AttributeSchema(GeneratedAttributeSchema):
25
35
  _sort_by: list[str] = ["name"]
26
36
  _enum_class: type[enum.Enum] | None = None
@@ -53,16 +63,36 @@ class AttributeSchema(GeneratedAttributeSchema):
53
63
 
54
64
  @model_validator(mode="before")
55
65
  @classmethod
56
- def validate_dropdown_choices(cls, values: dict[str, Any]) -> dict[str, Any]:
66
+ def validate_dropdown_choices(cls, values: Any) -> Any:
57
67
  """Validate that choices are defined for a dropdown but not for other kinds."""
58
- if values.get("kind") != "Dropdown" and values.get("choices"):
59
- raise ValueError(f"Can only specify 'choices' for kind=Dropdown: {values['kind']}")
60
-
61
- if values.get("kind") == "Dropdown" and not values.get("choices"):
68
+ if isinstance(values, dict):
69
+ kind = values.get("kind")
70
+ choices = values.get("choices")
71
+ elif isinstance(values, AttributeSchema):
72
+ kind = values.kind
73
+ choices = values.choices
74
+ else:
75
+ return values
76
+ if kind != "Dropdown" and choices:
77
+ raise ValueError(f"Can only specify 'choices' for kind=Dropdown: {kind}")
78
+
79
+ if kind == "Dropdown" and not choices:
62
80
  raise ValueError("The property 'choices' is required for kind=Dropdown")
63
81
 
64
82
  return values
65
83
 
84
+ @field_validator("parameters", mode="before")
85
+ @classmethod
86
+ def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
87
+ """Override parameters class if using base AttributeParameters class and should be using a subclass"""
88
+ kind = info.data["kind"]
89
+ expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
90
+ if value is None:
91
+ return expected_parameters_class()
92
+ if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
93
+ return expected_parameters_class(**value.model_dump())
94
+ return value
95
+
66
96
  def get_class(self) -> type[BaseAttribute]:
67
97
  return ATTRIBUTE_TYPES[self.kind].get_infrahub_class()
68
98
 
@@ -106,7 +136,7 @@ class AttributeSchema(GeneratedAttributeSchema):
106
136
 
107
137
  def to_node(self) -> dict[str, Any]:
108
138
  fields_to_exclude = {"id", "state", "filters"}
109
- fields_to_json = {"computed_attribute"}
139
+ fields_to_json = {"computed_attribute", "parameters"}
110
140
  data = self.model_dump(exclude=fields_to_exclude | fields_to_json)
111
141
 
112
142
  for field_name in fields_to_json:
@@ -117,6 +147,15 @@ class AttributeSchema(GeneratedAttributeSchema):
117
147
 
118
148
  return data
119
149
 
150
+ def get_regex(self) -> str | None:
151
+ return self.regex
152
+
153
+ def get_min_length(self) -> int | None:
154
+ return self.min_length
155
+
156
+ def get_max_length(self) -> int | None:
157
+ return self.max_length
158
+
120
159
  async def get_query_filter(
121
160
  self,
122
161
  name: str,
@@ -144,3 +183,39 @@ class AttributeSchema(GeneratedAttributeSchema):
144
183
  partial_match=partial_match,
145
184
  support_profiles=support_profiles,
146
185
  )
186
+
187
+
188
+ class TextAttributeSchema(AttributeSchema):
189
+ parameters: TextAttributeParameters = Field(
190
+ default_factory=TextAttributeParameters,
191
+ description="Extra parameters specific to text attributes",
192
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
193
+ )
194
+
195
+ @model_validator(mode="after")
196
+ def reconcile_parameters(self) -> Self:
197
+ if self.regex != self.parameters.regex:
198
+ final_regex = self.parameters.regex or self.regex
199
+ if not final_regex: # falsy parameters.regex override falsy regex
200
+ final_regex = self.parameters.regex
201
+ self.regex = self.parameters.regex = final_regex
202
+ if self.min_length != self.parameters.min_length:
203
+ final_min_length = self.parameters.min_length or self.min_length
204
+ if not final_min_length: # falsy parameters.min_length override falsy min_length
205
+ final_min_length = self.parameters.min_length
206
+ self.min_length = self.parameters.min_length = final_min_length
207
+ if self.max_length != self.parameters.max_length:
208
+ final_max_length = self.parameters.max_length or self.max_length
209
+ if not final_max_length: # falsy parameters.max_length override falsy max_length
210
+ final_max_length = self.parameters.max_length
211
+ self.max_length = self.parameters.max_length = final_max_length
212
+ return self
213
+
214
+ def get_regex(self) -> str | None:
215
+ return self.parameters.regex
216
+
217
+ def get_min_length(self) -> int | None:
218
+ return self.parameters.min_length
219
+
220
+ def get_max_length(self) -> int | None:
221
+ return self.parameters.max_length
@@ -13,7 +13,7 @@ from pydantic import field_validator
13
13
  from infrahub.core.constants import RelationshipCardinality, RelationshipKind
14
14
  from infrahub.core.models import HashableModel, HashableModelDiff
15
15
 
16
- from .attribute_schema import AttributeSchema
16
+ from .attribute_schema import AttributeSchema, get_attribute_schema_class_for_kind
17
17
  from .generated.base_node_schema import GeneratedBaseNodeSchema
18
18
  from .relationship_schema import RelationshipSchema
19
19
 
@@ -74,6 +74,30 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
74
74
  Be careful hash generated from hash() have a salt by default and they will not be the same across run"""
75
75
  return hash(self.get_hash())
76
76
 
77
+ @field_validator("attributes", mode="before")
78
+ @classmethod
79
+ def set_attribute_type(cls, raw_attributes: Any) -> Any:
80
+ if not isinstance(raw_attributes, list):
81
+ return raw_attributes
82
+ attribute_schemas_with_types: list[Any] = []
83
+ for raw_attr in raw_attributes:
84
+ if not isinstance(raw_attr, (dict, AttributeSchema)):
85
+ attribute_schemas_with_types.append(raw_attr)
86
+ continue
87
+ if isinstance(raw_attr, dict):
88
+ kind = raw_attr.get("kind")
89
+ attribute_type_class = get_attribute_schema_class_for_kind(kind=kind)
90
+ attribute_schemas_with_types.append(attribute_type_class(**raw_attr))
91
+ continue
92
+
93
+ expected_attr_schema_class = get_attribute_schema_class_for_kind(kind=raw_attr.kind)
94
+ if not isinstance(raw_attr, expected_attr_schema_class):
95
+ final_attr = expected_attr_schema_class(**raw_attr.model_dump())
96
+ else:
97
+ final_attr = raw_attr
98
+ attribute_schemas_with_types.append(final_attr)
99
+ return attribute_schemas_with_types
100
+
77
101
  def to_dict(self) -> dict:
78
102
  data = self.model_dump(
79
103
  exclude_unset=True, exclude_none=True, exclude_defaults=True, exclude={"attributes", "relationships"}
@@ -1,5 +1,17 @@
1
1
  from typing import Any
2
2
 
3
+ from infrahub.actions.schema import (
4
+ core_action,
5
+ core_generator_action,
6
+ core_group_action,
7
+ core_group_trigger_rule,
8
+ core_node_trigger_attribute_match,
9
+ core_node_trigger_match,
10
+ core_node_trigger_relationship_match,
11
+ core_node_trigger_rule,
12
+ core_trigger_rule,
13
+ )
14
+
3
15
  from ...generic_schema import GenericSchema
4
16
  from ...node_schema import NodeSchema
5
17
  from .account import (
@@ -63,6 +75,9 @@ from .webhook import core_custom_webhook, core_standard_webhook, core_webhook
63
75
 
64
76
  core_models_mixed: dict[str, list] = {
65
77
  "generics": [
78
+ core_action,
79
+ core_trigger_rule,
80
+ core_node_trigger_match,
66
81
  core_node,
67
82
  lineage_owner,
68
83
  core_profile_schema_definition,
@@ -90,12 +105,18 @@ core_models_mixed: dict[str, list] = {
90
105
  ],
91
106
  "nodes": [
92
107
  menu_item,
108
+ core_group_action,
93
109
  core_standard_group,
94
110
  core_generator_group,
95
111
  core_graphql_query_group,
96
112
  builtin_tag,
97
113
  core_account,
98
114
  core_account_token,
115
+ core_generator_action,
116
+ core_group_trigger_rule,
117
+ core_node_trigger_rule,
118
+ core_node_trigger_attribute_match,
119
+ core_node_trigger_relationship_match,
99
120
  core_password_credential,
100
121
  core_refresh_token,
101
122
  core_proposed_change,
@@ -31,6 +31,7 @@ from infrahub.core.constants import (
31
31
  RelationshipKind,
32
32
  UpdateSupport,
33
33
  )
34
+ from infrahub.core.schema.attribute_parameters import AttributeParameters
34
35
  from infrahub.core.schema.attribute_schema import AttributeSchema
35
36
  from infrahub.core.schema.computed_attribute import ComputedAttribute
36
37
  from infrahub.core.schema.dropdown import DropdownChoice
@@ -506,21 +507,21 @@ attribute_schema = SchemaNode(
506
507
  SchemaAttribute(
507
508
  name="regex",
508
509
  kind="Text",
509
- description="Regex uses to limit the characters allowed in for the attributes.",
510
+ description="Regex uses to limit the characters allowed in for the attributes. (deprecated: please use parameters.regex instead)",
510
511
  optional=True,
511
512
  extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
512
513
  ),
513
514
  SchemaAttribute(
514
515
  name="max_length",
515
516
  kind="Number",
516
- description="Set a maximum number of characters allowed for a given attribute.",
517
+ description="Set a maximum number of characters allowed for a given attribute. (deprecated: please use parameters.max_length instead)",
517
518
  optional=True,
518
519
  extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
519
520
  ),
520
521
  SchemaAttribute(
521
522
  name="min_length",
522
523
  kind="Number",
523
- description="Set a minimum number of characters allowed for a given attribute.",
524
+ description="Set a minimum number of characters allowed for a given attribute. (deprecated: please use parameters.min_length instead)",
524
525
  optional=True,
525
526
  extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
526
527
  ),
@@ -617,6 +618,15 @@ attribute_schema = SchemaNode(
617
618
  optional=True,
618
619
  extra={"update": UpdateSupport.ALLOWED},
619
620
  ),
621
+ SchemaAttribute(
622
+ name="parameters",
623
+ kind="JSON",
624
+ internal_kind=AttributeParameters,
625
+ optional=True,
626
+ description="Extra parameters specific to this kind of attribute",
627
+ extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
628
+ default_factory="AttributeParameters",
629
+ ),
620
630
  SchemaAttribute(
621
631
  name="deprecation",
622
632
  kind="Text",
@@ -8,6 +8,7 @@ from pydantic import Field
8
8
 
9
9
  from infrahub.core.constants import AllowOverrideType, BranchSupportType, HashableModelState
10
10
  from infrahub.core.models import HashableModel
11
+ from infrahub.core.schema.attribute_parameters import AttributeParameters # noqa: TC001
11
12
  from infrahub.core.schema.computed_attribute import ComputedAttribute # noqa: TC001
12
13
  from infrahub.core.schema.dropdown import DropdownChoice # noqa: TC001
13
14
 
@@ -44,17 +45,17 @@ class GeneratedAttributeSchema(HashableModel):
44
45
  )
45
46
  regex: str | None = Field(
46
47
  default=None,
47
- description="Regex uses to limit the characters allowed in for the attributes.",
48
+ description="Regex uses to limit the characters allowed in for the attributes. (deprecated: please use parameters.regex instead)",
48
49
  json_schema_extra={"update": "validate_constraint"},
49
50
  )
50
51
  max_length: int | None = Field(
51
52
  default=None,
52
- description="Set a maximum number of characters allowed for a given attribute.",
53
+ description="Set a maximum number of characters allowed for a given attribute. (deprecated: please use parameters.max_length instead)",
53
54
  json_schema_extra={"update": "validate_constraint"},
54
55
  )
55
56
  min_length: int | None = Field(
56
57
  default=None,
57
- description="Set a minimum number of characters allowed for a given attribute.",
58
+ description="Set a minimum number of characters allowed for a given attribute. (deprecated: please use parameters.min_length instead)",
58
59
  json_schema_extra={"update": "validate_constraint"},
59
60
  )
60
61
  label: str | None = Field(
@@ -112,6 +113,11 @@ class GeneratedAttributeSchema(HashableModel):
112
113
  description="Type of allowed override for the attribute.",
113
114
  json_schema_extra={"update": "allowed"},
114
115
  )
116
+ parameters: AttributeParameters = Field(
117
+ default_factory=AttributeParameters,
118
+ description="Extra parameters specific to this kind of attribute",
119
+ json_schema_extra={"update": "validate_constraint"},
120
+ )
115
121
  deprecation: str | None = Field(
116
122
  default=None,
117
123
  description="Mark attribute as deprecated and provide a user-friendly message to display",
@@ -49,6 +49,7 @@ from infrahub.core.schema import (
49
49
  SchemaRoot,
50
50
  TemplateSchema,
51
51
  )
52
+ from infrahub.core.schema.attribute_schema import get_attribute_schema_class_for_kind
52
53
  from infrahub.core.schema.definitions.core import core_profile_schema_definition
53
54
  from infrahub.core.validators import CONSTRAINT_VALIDATOR_MAP
54
55
  from infrahub.exceptions import SchemaNotFoundError, ValidationError
@@ -1136,7 +1137,7 @@ class SchemaBranch:
1136
1137
  self.set(name=name, schema=node)
1137
1138
 
1138
1139
  def process_labels(self) -> None:
1139
- def check_if_need_to_update_label(node) -> bool:
1140
+ def check_if_need_to_update_label(node: MainSchemaTypes) -> bool:
1140
1141
  if not node.label:
1141
1142
  return True
1142
1143
  for item in node.relationships + node.attributes:
@@ -1812,12 +1813,14 @@ class SchemaBranch:
1812
1813
  def generate_profile_from_node(self, node: NodeSchema) -> ProfileSchema:
1813
1814
  core_profile_schema = self.get(name=InfrahubKind.PROFILE, duplicate=False)
1814
1815
  core_name_attr = core_profile_schema.get_attribute(name="profile_name")
1815
- profile_name_attr = AttributeSchema(
1816
+ name_attr_schema_class = get_attribute_schema_class_for_kind(kind=core_name_attr.kind)
1817
+ profile_name_attr = name_attr_schema_class(
1816
1818
  **core_name_attr.model_dump(exclude=["id", "inherited"]),
1817
1819
  )
1818
1820
  profile_name_attr.branch = node.branch
1819
1821
  core_priority_attr = core_profile_schema.get_attribute(name="profile_priority")
1820
- profile_priority_attr = AttributeSchema(
1822
+ priority_attr_schema_class = get_attribute_schema_class_for_kind(kind=core_priority_attr.kind)
1823
+ profile_priority_attr = priority_attr_schema_class(
1821
1824
  **core_priority_attr.model_dump(exclude=["id", "inherited"]),
1822
1825
  )
1823
1826
  profile_priority_attr.branch = node.branch
@@ -1848,8 +1851,8 @@ class SchemaBranch:
1848
1851
  for node_attr in node.attributes:
1849
1852
  if node_attr.read_only or node_attr.optional is False:
1850
1853
  continue
1851
-
1852
- attr = AttributeSchema(
1854
+ attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
1855
+ attr = attr_schema_class(
1853
1856
  optional=True,
1854
1857
  **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "default_value", "inherited"]),
1855
1858
  )
@@ -1973,7 +1976,8 @@ class SchemaBranch:
1973
1976
  else self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
1974
1977
  )
1975
1978
  core_name_attr = core_template_schema.get_attribute(name=OBJECT_TEMPLATE_NAME_ATTR)
1976
- template_name_attr = AttributeSchema(
1979
+ name_attr_schema_class = get_attribute_schema_class_for_kind(kind=core_name_attr.kind)
1980
+ template_name_attr = name_attr_schema_class(
1977
1981
  **core_name_attr.model_dump(exclude=["id", "inherited"]),
1978
1982
  )
1979
1983
  template_name_attr.branch = node.branch
@@ -2033,7 +2037,8 @@ class SchemaBranch:
2033
2037
  if node_attr.unique or node_attr.read_only:
2034
2038
  continue
2035
2039
 
2036
- attr = AttributeSchema(
2040
+ attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
2041
+ attr = attr_schema_class(
2037
2042
  optional=node_attr.optional if is_autogenerated_subtemplate else True,
2038
2043
  **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only"]),
2039
2044
  )
@@ -5,6 +5,7 @@ from .attribute.length import AttributeLengthChecker
5
5
  from .attribute.optional import AttributeOptionalChecker
6
6
  from .attribute.regex import AttributeRegexChecker
7
7
  from .attribute.unique import AttributeUniquenessChecker
8
+ from .enum import ConstraintIdentifier
8
9
  from .interface import ConstraintCheckerInterface
9
10
  from .node.attribute import NodeAttributeAddChecker
10
11
  from .node.generate_profile import NodeGenerateProfileChecker
@@ -17,11 +18,14 @@ from .relationship.peer import RelationshipPeerChecker
17
18
  from .uniqueness.checker import UniquenessChecker
18
19
 
19
20
  CONSTRAINT_VALIDATOR_MAP: dict[str, type[ConstraintCheckerInterface] | None] = {
21
+ "attribute.kind.update": AttributeKindChecker,
20
22
  "attribute.regex.update": AttributeRegexChecker,
23
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_REGEX_UPDATE.value: AttributeRegexChecker,
21
24
  "attribute.enum.update": AttributeEnumChecker,
22
- "attribute.kind.update": AttributeKindChecker,
23
25
  "attribute.min_length.update": AttributeLengthChecker,
24
26
  "attribute.max_length.update": AttributeLengthChecker,
27
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MIN_LENGTH_UPDATE.value: AttributeLengthChecker,
28
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MAX_LENGTH_UPDATE.value: AttributeLengthChecker,
25
29
  "attribute.unique.update": AttributeUniquenessChecker,
26
30
  "attribute.optional.update": AttributeOptionalChecker,
27
31
  "attribute.choices.update": AttributeChoicesChecker,
@@ -31,8 +31,7 @@ class AttributeChoicesUpdateValidatorQuery(AttributeSchemaValidatorQuery):
31
31
 
32
32
  query = """
33
33
  MATCH p = (n:%(node_kind)s)
34
- CALL {
35
- WITH n
34
+ CALL (n) {
36
35
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
37
36
  WHERE all(
38
37
  r in relationships(path)
@@ -30,8 +30,7 @@ class AttributeEnumUpdateValidatorQuery(AttributeSchemaValidatorQuery):
30
30
  self.params["null_value"] = NULL_VALUE
31
31
  query = """
32
32
  MATCH (n:%(node_kind)s)
33
- CALL {
34
- WITH n
33
+ CALL (n) {
35
34
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
36
35
  WHERE all(
37
36
  r in relationships(path)
@@ -37,8 +37,7 @@ class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
37
37
 
38
38
  query = """
39
39
  MATCH p = (n:%(node_kind)s)
40
- CALL {
41
- WITH n
40
+ CALL (n) {
42
41
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
43
42
  WHERE all(
44
43
  r in relationships(path)
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from infrahub.core.constants import PathType
6
6
  from infrahub.core.path import DataPath, GroupedDataPaths
7
+ from infrahub.core.validators.enum import ConstraintIdentifier
7
8
 
8
9
  from ..interface import ConstraintCheckerInterface
9
10
  from ..shared import AttributeSchemaValidatorQuery
@@ -23,13 +24,12 @@ class AttributeLengthUpdateValidatorQuery(AttributeSchemaValidatorQuery):
23
24
  self.params.update(branch_params)
24
25
 
25
26
  self.params["attr_name"] = self.attribute_schema.name
26
- self.params["min_length"] = self.attribute_schema.min_length
27
- self.params["max_length"] = self.attribute_schema.max_length
27
+ self.params["min_length"] = self.attribute_schema.get_min_length()
28
+ self.params["max_length"] = self.attribute_schema.get_max_length()
28
29
 
29
30
  query = """
30
31
  MATCH (n:%(node_kind)s)
31
- CALL {
32
- WITH n
32
+ CALL (n) {
33
33
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
34
34
  WHERE all(
35
35
  r in relationships(path)
@@ -79,14 +79,21 @@ class AttributeLengthChecker(ConstraintCheckerInterface):
79
79
  return "attribute.length.update"
80
80
 
81
81
  def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
82
- return request.constraint_name in ("attribute.min_length.update", "attribute.max_length.update")
82
+ return request.constraint_name in (
83
+ "attribute.min_length.update",
84
+ "attribute.max_length.update",
85
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MIN_LENGTH_UPDATE.value,
86
+ ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MAX_LENGTH_UPDATE.value,
87
+ )
83
88
 
84
89
  async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
85
90
  grouped_data_paths_list: list[GroupedDataPaths] = []
86
91
  if not request.schema_path.field_name:
87
92
  raise ValueError("field_name is not defined")
88
93
  attribute_schema = request.node_schema.get_attribute(name=request.schema_path.field_name)
89
- if attribute_schema.min_length is None and attribute_schema.max_length is True:
94
+ min_length = attribute_schema.get_min_length()
95
+ max_length = attribute_schema.get_max_length()
96
+ if min_length is None and max_length is None:
90
97
  return grouped_data_paths_list
91
98
 
92
99
  for query_class in self.query_classes:
@@ -27,8 +27,7 @@ class AttributeOptionalUpdateValidatorQuery(AttributeSchemaValidatorQuery):
27
27
 
28
28
  query = """
29
29
  MATCH (n:%(node_kind)s)
30
- CALL {
31
- WITH n
30
+ CALL (n) {
32
31
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
33
32
  WHERE all(
34
33
  r in relationships(path)
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from infrahub.core.constants import NULL_VALUE, PathType
6
6
  from infrahub.core.path import DataPath, GroupedDataPaths
7
+ from infrahub.core.validators.enum import ConstraintIdentifier
7
8
 
8
9
  from ..interface import ConstraintCheckerInterface
9
10
  from ..shared import AttributeSchemaValidatorQuery
@@ -23,12 +24,11 @@ class AttributeRegexUpdateValidatorQuery(AttributeSchemaValidatorQuery):
23
24
  self.params.update(branch_params)
24
25
 
25
26
  self.params["attr_name"] = self.attribute_schema.name
26
- self.params["attr_value_regex"] = self.attribute_schema.regex
27
+ self.params["attr_value_regex"] = self.attribute_schema.get_regex()
27
28
  self.params["null_value"] = NULL_VALUE
28
29
  query = """
29
30
  MATCH p = (n:%(node_kind)s)
30
- CALL {
31
- WITH n
31
+ CALL (n) {
32
32
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
33
33
  WHERE all(
34
34
  r in relationships(path)
@@ -79,14 +79,14 @@ class AttributeRegexChecker(ConstraintCheckerInterface):
79
79
  return "attribute.regex.update"
80
80
 
81
81
  def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
82
- return request.constraint_name == self.name
82
+ return request.constraint_name in (self.name, ConstraintIdentifier.ATTRIBUTE_PARAMETERS_REGEX_UPDATE.value)
83
83
 
84
84
  async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
85
85
  grouped_data_paths_list: list[GroupedDataPaths] = []
86
86
  if not request.schema_path.field_name:
87
87
  raise ValueError("field_name is not defined")
88
88
  attribute_schema = request.node_schema.get_attribute(name=request.schema_path.field_name)
89
- if not attribute_schema.regex:
89
+ if not attribute_schema.get_regex():
90
90
  return grouped_data_paths_list
91
91
 
92
92
  for query_class in self.query_classes:
@@ -31,12 +31,10 @@ class AttributeUniqueUpdateValidatorQuery(AttributeSchemaValidatorQuery):
31
31
  query = """
32
32
  MATCH (potential_node:Node)
33
33
  WHERE $node_kind IN LABELS(potential_node)
34
- CALL {
35
- WITH potential_node
34
+ CALL (potential_node) {
36
35
  MATCH potential_path = (potential_node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name })-[potential_value_relationship:HAS_VALUE]-(potential_value:AttributeValue)
37
36
  WHERE all(r IN relationships(potential_path) WHERE (%(branch_filter)s))
38
37
  WITH
39
- potential_node,
40
38
  potential_value,
41
39
  potential_value_relationship,
42
40
  potential_path,