infrahub-server 1.2.11__py3-none-any.whl → 1.3.0__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 (211) hide show
  1. infrahub/actions/constants.py +130 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +243 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +393 -0
  6. infrahub/actions/tasks.py +119 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/branch/__init__.py +0 -0
  9. infrahub/branch/tasks.py +29 -0
  10. infrahub/branch/triggers.py +22 -0
  11. infrahub/cli/db.py +3 -4
  12. infrahub/computed_attribute/gather.py +3 -1
  13. infrahub/computed_attribute/tasks.py +23 -29
  14. infrahub/core/account.py +24 -47
  15. infrahub/core/attribute.py +13 -15
  16. infrahub/core/constants/__init__.py +10 -0
  17. infrahub/core/constants/database.py +1 -0
  18. infrahub/core/constants/infrahubkind.py +9 -0
  19. infrahub/core/constraint/node/runner.py +3 -1
  20. infrahub/core/convert_object_type/__init__.py +0 -0
  21. infrahub/core/convert_object_type/conversion.py +124 -0
  22. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  23. infrahub/core/diff/coordinator.py +8 -1
  24. infrahub/core/diff/query/all_conflicts.py +1 -5
  25. infrahub/core/diff/query/artifact.py +10 -20
  26. infrahub/core/diff/query/delete_query.py +8 -4
  27. infrahub/core/diff/query/diff_get.py +3 -6
  28. infrahub/core/diff/query/field_specifiers.py +1 -1
  29. infrahub/core/diff/query/field_summary.py +2 -4
  30. infrahub/core/diff/query/merge.py +72 -125
  31. infrahub/core/diff/query/save.py +83 -68
  32. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  33. infrahub/core/diff/query/time_range_query.py +0 -1
  34. infrahub/core/diff/repository/repository.py +4 -0
  35. infrahub/core/graph/__init__.py +1 -1
  36. infrahub/core/manager.py +14 -11
  37. infrahub/core/migrations/graph/__init__.py +6 -0
  38. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  39. infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
  40. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -6
  41. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  42. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  43. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  44. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  45. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  46. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
  47. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  48. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  49. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +662 -0
  50. infrahub/core/migrations/graph/m030_illegal_edges.py +82 -0
  51. infrahub/core/migrations/query/attribute_add.py +14 -11
  52. infrahub/core/migrations/query/attribute_rename.py +6 -11
  53. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  54. infrahub/core/migrations/query/node_duplicate.py +19 -21
  55. infrahub/core/migrations/query/relationship_duplicate.py +19 -18
  56. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  57. infrahub/core/migrations/schema/node_remove.py +19 -20
  58. infrahub/core/models.py +29 -2
  59. infrahub/core/node/__init__.py +131 -28
  60. infrahub/core/node/base.py +1 -1
  61. infrahub/core/node/create.py +211 -0
  62. infrahub/core/node/resource_manager/number_pool.py +31 -5
  63. infrahub/core/node/standard.py +6 -1
  64. infrahub/core/path.py +15 -1
  65. infrahub/core/protocols.py +57 -0
  66. infrahub/core/protocols_base.py +3 -0
  67. infrahub/core/query/__init__.py +2 -2
  68. infrahub/core/query/delete.py +3 -3
  69. infrahub/core/query/diff.py +19 -32
  70. infrahub/core/query/ipam.py +10 -20
  71. infrahub/core/query/node.py +29 -47
  72. infrahub/core/query/relationship.py +55 -34
  73. infrahub/core/query/resource_manager.py +1 -2
  74. infrahub/core/query/standard_node.py +19 -5
  75. infrahub/core/query/subquery.py +2 -4
  76. infrahub/core/relationship/constraints/count.py +10 -9
  77. infrahub/core/relationship/constraints/interface.py +2 -1
  78. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  79. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  80. infrahub/core/relationship/constraints/peer_relatives.py +72 -0
  81. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  82. infrahub/core/relationship/model.py +4 -1
  83. infrahub/core/schema/__init__.py +2 -1
  84. infrahub/core/schema/attribute_parameters.py +160 -0
  85. infrahub/core/schema/attribute_schema.py +130 -7
  86. infrahub/core/schema/basenode_schema.py +27 -3
  87. infrahub/core/schema/definitions/core/__init__.py +29 -1
  88. infrahub/core/schema/definitions/core/group.py +45 -0
  89. infrahub/core/schema/definitions/core/resource_pool.py +9 -0
  90. infrahub/core/schema/definitions/internal.py +43 -5
  91. infrahub/core/schema/generated/attribute_schema.py +16 -3
  92. infrahub/core/schema/generated/relationship_schema.py +11 -1
  93. infrahub/core/schema/manager.py +7 -2
  94. infrahub/core/schema/schema_branch.py +109 -12
  95. infrahub/core/validators/__init__.py +15 -2
  96. infrahub/core/validators/attribute/choices.py +1 -3
  97. infrahub/core/validators/attribute/enum.py +1 -3
  98. infrahub/core/validators/attribute/kind.py +1 -3
  99. infrahub/core/validators/attribute/length.py +13 -7
  100. infrahub/core/validators/attribute/min_max.py +118 -0
  101. infrahub/core/validators/attribute/number_pool.py +106 -0
  102. infrahub/core/validators/attribute/optional.py +1 -4
  103. infrahub/core/validators/attribute/regex.py +5 -6
  104. infrahub/core/validators/attribute/unique.py +1 -3
  105. infrahub/core/validators/determiner.py +18 -2
  106. infrahub/core/validators/enum.py +12 -0
  107. infrahub/core/validators/node/hierarchy.py +3 -6
  108. infrahub/core/validators/query.py +1 -3
  109. infrahub/core/validators/relationship/count.py +6 -12
  110. infrahub/core/validators/relationship/optional.py +2 -4
  111. infrahub/core/validators/relationship/peer.py +177 -12
  112. infrahub/core/validators/tasks.py +1 -1
  113. infrahub/core/validators/uniqueness/query.py +5 -9
  114. infrahub/database/__init__.py +12 -4
  115. infrahub/database/validation.py +100 -0
  116. infrahub/dependencies/builder/constraint/grouped/node_runner.py +4 -0
  117. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  118. infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
  119. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  120. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  121. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  122. infrahub/dependencies/registry.py +4 -0
  123. infrahub/events/group_action.py +1 -0
  124. infrahub/events/models.py +1 -1
  125. infrahub/git/base.py +5 -3
  126. infrahub/git/integrator.py +96 -5
  127. infrahub/git/tasks.py +1 -0
  128. infrahub/graphql/analyzer.py +139 -18
  129. infrahub/graphql/manager.py +4 -0
  130. infrahub/graphql/mutations/action.py +164 -0
  131. infrahub/graphql/mutations/convert_object_type.py +71 -0
  132. infrahub/graphql/mutations/main.py +25 -176
  133. infrahub/graphql/mutations/proposed_change.py +20 -17
  134. infrahub/graphql/mutations/relationship.py +32 -0
  135. infrahub/graphql/mutations/resource_manager.py +63 -7
  136. infrahub/graphql/queries/convert_object_type_mapping.py +34 -0
  137. infrahub/graphql/queries/resource_manager.py +7 -1
  138. infrahub/graphql/resolvers/many_relationship.py +1 -1
  139. infrahub/graphql/resolvers/resolver.py +2 -2
  140. infrahub/graphql/resolvers/single_relationship.py +1 -1
  141. infrahub/graphql/schema.py +6 -0
  142. infrahub/menu/menu.py +34 -2
  143. infrahub/message_bus/messages/__init__.py +0 -10
  144. infrahub/message_bus/operations/__init__.py +0 -8
  145. infrahub/message_bus/operations/refresh/registry.py +4 -7
  146. infrahub/patch/queries/delete_duplicated_edges.py +45 -39
  147. infrahub/pools/models.py +14 -0
  148. infrahub/pools/number.py +5 -3
  149. infrahub/pools/registration.py +22 -0
  150. infrahub/pools/tasks.py +126 -0
  151. infrahub/prefect_server/models.py +1 -19
  152. infrahub/proposed_change/models.py +68 -3
  153. infrahub/proposed_change/tasks.py +911 -34
  154. infrahub/schema/__init__.py +0 -0
  155. infrahub/schema/tasks.py +27 -0
  156. infrahub/schema/triggers.py +23 -0
  157. infrahub/task_manager/models.py +10 -6
  158. infrahub/trigger/catalogue.py +6 -0
  159. infrahub/trigger/models.py +23 -6
  160. infrahub/trigger/setup.py +26 -2
  161. infrahub/trigger/tasks.py +4 -2
  162. infrahub/types.py +6 -0
  163. infrahub/webhook/tasks.py +6 -9
  164. infrahub/workflows/catalogue.py +103 -1
  165. infrahub_sdk/client.py +43 -10
  166. infrahub_sdk/ctl/generator.py +4 -4
  167. infrahub_sdk/ctl/repository.py +1 -1
  168. infrahub_sdk/node/__init__.py +39 -0
  169. infrahub_sdk/node/attribute.py +122 -0
  170. infrahub_sdk/node/constants.py +21 -0
  171. infrahub_sdk/{node.py → node/node.py} +158 -803
  172. infrahub_sdk/node/parsers.py +15 -0
  173. infrahub_sdk/node/property.py +24 -0
  174. infrahub_sdk/node/related_node.py +266 -0
  175. infrahub_sdk/node/relationship.py +302 -0
  176. infrahub_sdk/protocols.py +112 -0
  177. infrahub_sdk/protocols_base.py +34 -2
  178. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  179. infrahub_sdk/query_groups.py +17 -5
  180. infrahub_sdk/schema/main.py +1 -0
  181. infrahub_sdk/schema/repository.py +16 -0
  182. infrahub_sdk/spec/object.py +1 -1
  183. infrahub_sdk/store.py +1 -1
  184. infrahub_sdk/testing/schemas/car_person.py +1 -0
  185. infrahub_sdk/utils.py +7 -20
  186. infrahub_sdk/yaml.py +6 -5
  187. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/METADATA +5 -5
  188. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/RECORD +197 -168
  189. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/WHEEL +1 -1
  190. infrahub_testcontainers/container.py +239 -65
  191. infrahub_testcontainers/docker-compose-cluster.test.yml +321 -0
  192. infrahub_testcontainers/docker-compose.test.yml +2 -1
  193. infrahub_testcontainers/helpers.py +23 -3
  194. infrahub_testcontainers/plugin.py +9 -0
  195. infrahub/message_bus/messages/check_generator_run.py +0 -26
  196. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  197. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  198. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  199. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  200. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  201. infrahub/message_bus/operations/check/__init__.py +0 -3
  202. infrahub/message_bus/operations/check/generator.py +0 -156
  203. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  204. infrahub/message_bus/operations/finalize/validator.py +0 -133
  205. infrahub/message_bus/operations/requests/__init__.py +0 -9
  206. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  207. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  208. infrahub/patch/queries/consolidate_duplicated_nodes.py +0 -109
  209. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  210. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/LICENSE.txt +0 -0
  211. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Mapping
4
+
5
+ from infrahub.exceptions import ValidationError
6
+
7
+ from .interface import RelationshipManagerConstraintInterface
8
+
9
+ if TYPE_CHECKING:
10
+ from infrahub.core.branch import Branch
11
+ from infrahub.core.node import Node
12
+ from infrahub.core.schema import MainSchemaTypes
13
+ from infrahub.database import InfrahubDatabase
14
+
15
+ from ..model import RelationshipManager
16
+
17
+
18
+ class RelationshipPeerParentConstraint(RelationshipManagerConstraintInterface):
19
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
20
+ self.db = db
21
+ self.branch = branch
22
+
23
+ async def _check_relationship_peers_parent(
24
+ self, relm: RelationshipManager, parent_rel_name: str, node: Node, peers: Mapping[str, Node]
25
+ ) -> None:
26
+ """Validate that all peers of a given `relm` have the same parent for the given `relationship_name`."""
27
+ node_parent = await node.get_parent_relationship_peer(db=self.db, name=parent_rel_name)
28
+ if not node_parent:
29
+ # If the schema is properly validated we are not expecting this to happen
30
+ raise ValidationError(f"Node {node.id} ({node.get_kind()}) does not have a parent peer")
31
+
32
+ parents: set[str] = {node_parent.id}
33
+ for peer in peers.values():
34
+ parent = await peer.get_parent_relationship_peer(db=self.db, name=parent_rel_name)
35
+ if not parent:
36
+ # If the schema is properly validated we are not expecting this to happen
37
+ raise ValidationError(f"Peer {peer.id} ({peer.get_kind()}) does not have a parent peer")
38
+ parents.add(parent.id)
39
+
40
+ if len(parents) != 1:
41
+ raise ValidationError(
42
+ f"All the elements of the '{relm.name}' relationship on node {node.id} ({node.get_kind()}) must have the same parent "
43
+ "as the node"
44
+ )
45
+
46
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
47
+ if not relm.schema.common_parent:
48
+ return
49
+
50
+ peers = await relm.get_peers(db=self.db)
51
+ if not peers:
52
+ return
53
+
54
+ await self._check_relationship_peers_parent(
55
+ relm=relm, parent_rel_name=relm.schema.common_parent, node=node, peers=peers
56
+ )
@@ -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, node: Node) -> None: # noqa: ARG002
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
+ )
@@ -23,7 +23,7 @@ class RelationshipProfilesKindConstraint(RelationshipManagerConstraintInterface)
23
23
  self.branch = branch
24
24
  self.schema_branch = registry.schema.get_schema_branch(branch.name if branch else registry.default_branch)
25
25
 
26
- async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None:
26
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
27
27
  if relm.name != "profiles" or not isinstance(node_schema, NodeSchema):
28
28
  return
29
29
 
@@ -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
@@ -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,160 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Self
5
+
6
+ from pydantic import ConfigDict, Field, model_validator
7
+
8
+ from infrahub import config
9
+ from infrahub.core.constants.schema import UpdateSupport
10
+ from infrahub.core.models import HashableModel
11
+
12
+
13
+ def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParameters]:
14
+ param_classes: dict[str, type[AttributeParameters]] = {
15
+ "NumberPool": NumberPoolParameters,
16
+ "Text": TextAttributeParameters,
17
+ "TextArea": TextAttributeParameters,
18
+ "Number": NumberAttributeParameters,
19
+ }
20
+ return param_classes.get(kind, AttributeParameters)
21
+
22
+
23
+ class AttributeParameters(HashableModel):
24
+ model_config = ConfigDict(extra="forbid")
25
+
26
+
27
+ class TextAttributeParameters(AttributeParameters):
28
+ regex: str | None = Field(
29
+ default=None,
30
+ description="Regular expression that attribute value must match if defined",
31
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
32
+ )
33
+ min_length: int | None = Field(
34
+ default=None,
35
+ description="Set a minimum number of characters allowed.",
36
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
37
+ )
38
+ max_length: int | None = Field(
39
+ default=None,
40
+ description="Set a maximum number of characters allowed.",
41
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
42
+ )
43
+
44
+ @model_validator(mode="after")
45
+ def validate_min_max(self) -> Self:
46
+ if (
47
+ config.SETTINGS.initialized
48
+ and config.SETTINGS.main.schema_strict_mode
49
+ and self.min_length is not None
50
+ and self.max_length is not None
51
+ ):
52
+ if self.min_length > self.max_length:
53
+ raise ValueError(
54
+ "`max_length` can't be less than `min_length` when the schema is configured with strict mode"
55
+ )
56
+
57
+ return self
58
+
59
+
60
+ class NumberAttributeParameters(AttributeParameters):
61
+ min_value: int | None = Field(
62
+ default=None,
63
+ description="Set a minimum value allowed.",
64
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
65
+ )
66
+ max_value: int | None = Field(
67
+ default=None,
68
+ description="Set a maximum value allowed.",
69
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
70
+ )
71
+ excluded_values: str | None = Field(
72
+ default=None,
73
+ description="List of values or range of values not allowed for the attribute, format is: '100,150-200,280,300-400'",
74
+ pattern=r"^(\d+(?:-\d+)?)(?:,\d+(?:-\d+)?)*$",
75
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
76
+ )
77
+
78
+ @model_validator(mode="after")
79
+ def validate_ranges(self) -> Self:
80
+ ranges = self.get_excluded_ranges()
81
+ for i, (start_range_1, end_range_1) in enumerate(ranges):
82
+ if start_range_1 > end_range_1:
83
+ raise ValueError("`start_range` can't be less than `end_range`")
84
+
85
+ # Check for overlapping ranges
86
+ for start_range_2, end_range_2 in ranges[i + 1 :]:
87
+ if not (end_range_1 < start_range_2 or start_range_1 > end_range_2):
88
+ raise ValueError("Excluded ranges cannot overlap")
89
+
90
+ return self
91
+
92
+ @model_validator(mode="after")
93
+ def validate_min_max(self) -> Self:
94
+ if (
95
+ config.SETTINGS.initialized
96
+ and config.SETTINGS.main.schema_strict_mode
97
+ and self.min_value is not None
98
+ and self.max_value is not None
99
+ ):
100
+ if self.min_value > self.max_value:
101
+ raise ValueError(
102
+ "`max_value` can't be less than `min_value` when the schema is configured with strict mode"
103
+ )
104
+
105
+ return self
106
+
107
+ def get_excluded_single_values(self) -> list[int]:
108
+ if not self.excluded_values:
109
+ return []
110
+
111
+ results = [int(value) for value in self.excluded_values.split(",") if "-" not in value]
112
+ return results
113
+
114
+ def get_excluded_ranges(self) -> list[tuple[int, int]]:
115
+ if not self.excluded_values:
116
+ return []
117
+
118
+ ranges = []
119
+ for value in self.excluded_values.split(","):
120
+ if "-" in value:
121
+ start, end = map(int, value.split("-"))
122
+ ranges.append((start, end))
123
+
124
+ return ranges
125
+
126
+ def is_valid_value(self, value: int) -> bool:
127
+ if self.min_value is not None and value < self.min_value:
128
+ return False
129
+ if self.max_value is not None and value > self.max_value:
130
+ return False
131
+ if value in self.get_excluded_single_values():
132
+ return False
133
+ for start, end in self.get_excluded_ranges():
134
+ if start <= value <= end:
135
+ return False
136
+ return True
137
+
138
+
139
+ class NumberPoolParameters(AttributeParameters):
140
+ end_range: int = Field(
141
+ default=sys.maxsize,
142
+ description="End range for numbers for the associated NumberPool",
143
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
144
+ )
145
+ start_range: int = Field(
146
+ default=1,
147
+ description="Start range for numbers for the associated NumberPool",
148
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
149
+ )
150
+ number_pool_id: str | None = Field(
151
+ default=None,
152
+ description="The ID of the numberpool associated with this attribute",
153
+ json_schema_extra={"update": UpdateSupport.NOT_SUPPORTED.value},
154
+ )
155
+
156
+ @model_validator(mode="after")
157
+ def validate_ranges(self) -> Self:
158
+ if self.start_range > self.end_range:
159
+ raise ValueError("`start_range` can't be less than `end_range`")
160
+ return self
@@ -2,15 +2,23 @@ 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 (
16
+ AttributeParameters,
17
+ NumberAttributeParameters,
18
+ NumberPoolParameters,
19
+ TextAttributeParameters,
20
+ get_attribute_parameters_class_for_kind,
21
+ )
14
22
  from .generated.attribute_schema import GeneratedAttributeSchema
15
23
 
16
24
  if TYPE_CHECKING:
@@ -21,10 +29,32 @@ if TYPE_CHECKING:
21
29
  from infrahub.database import InfrahubDatabase
22
30
 
23
31
 
32
+ def get_attribute_schema_class_for_kind(kind: str) -> type[AttributeSchema]:
33
+ return attribute_schema_class_by_kind.get(kind, AttributeSchema)
34
+
35
+
24
36
  class AttributeSchema(GeneratedAttributeSchema):
25
37
  _sort_by: list[str] = ["name"]
26
38
  _enum_class: type[enum.Enum] | None = None
27
39
 
40
+ @classmethod
41
+ def model_json_schema(cls, *args: Any, **kwargs: Any) -> dict[str, Any]:
42
+ schema = super().model_json_schema(*args, **kwargs)
43
+
44
+ # Build conditional schema based on attribute_schema_class_by_kind mapping
45
+ # This override allows people using the Yaml language server to get the correct mappings
46
+ # for the parameters when selecting the appropriate kind
47
+ schema["allOf"] = []
48
+ for kind, schema_class in attribute_schema_class_by_kind.items():
49
+ schema["allOf"].append(
50
+ {
51
+ "if": {"properties": {"kind": {"const": kind}}},
52
+ "then": {"properties": {"parameters": {"$ref": f"#/definitions/{schema_class.__name__}"}}},
53
+ }
54
+ )
55
+
56
+ return schema
57
+
28
58
  @property
29
59
  def is_attribute(self) -> bool:
30
60
  return True
@@ -53,16 +83,46 @@ class AttributeSchema(GeneratedAttributeSchema):
53
83
 
54
84
  @model_validator(mode="before")
55
85
  @classmethod
56
- def validate_dropdown_choices(cls, values: dict[str, Any]) -> dict[str, Any]:
86
+ def validate_dropdown_choices(cls, values: Any) -> Any:
57
87
  """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']}")
88
+ if isinstance(values, dict):
89
+ kind = values.get("kind")
90
+ choices = values.get("choices")
91
+ elif isinstance(values, AttributeSchema):
92
+ kind = values.kind
93
+ choices = values.choices
94
+ else:
95
+ return values
96
+ if kind != "Dropdown" and choices:
97
+ raise ValueError(f"Can only specify 'choices' for kind=Dropdown: {kind}")
60
98
 
61
- if values.get("kind") == "Dropdown" and not values.get("choices"):
99
+ if kind == "Dropdown" and not choices:
62
100
  raise ValueError("The property 'choices' is required for kind=Dropdown")
63
101
 
64
102
  return values
65
103
 
104
+ @field_validator("parameters", mode="before")
105
+ @classmethod
106
+ def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
107
+ """Override parameters class if using base AttributeParameters class and should be using a subclass"""
108
+ kind = info.data["kind"]
109
+ expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
110
+ if value is None:
111
+ return expected_parameters_class()
112
+ if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
113
+ return expected_parameters_class(**value.model_dump())
114
+ return value
115
+
116
+ @model_validator(mode="after")
117
+ def validate_parameters(self) -> Self:
118
+ if isinstance(self.parameters, NumberPoolParameters) and not self.kind == "NumberPool":
119
+ raise ValueError(f"NumberPoolParameters can't be used as parameters for {self.kind}")
120
+
121
+ if isinstance(self.parameters, TextAttributeParameters) and self.kind not in ["Text", "TextArea"]:
122
+ raise ValueError(f"TextAttributeParameters can't be used as parameters for {self.kind}")
123
+
124
+ return self
125
+
66
126
  def get_class(self) -> type[BaseAttribute]:
67
127
  return ATTRIBUTE_TYPES[self.kind].get_infrahub_class()
68
128
 
@@ -106,7 +166,7 @@ class AttributeSchema(GeneratedAttributeSchema):
106
166
 
107
167
  def to_node(self) -> dict[str, Any]:
108
168
  fields_to_exclude = {"id", "state", "filters"}
109
- fields_to_json = {"computed_attribute"}
169
+ fields_to_json = {"computed_attribute", "parameters"}
110
170
  data = self.model_dump(exclude=fields_to_exclude | fields_to_json)
111
171
 
112
172
  for field_name in fields_to_json:
@@ -117,6 +177,15 @@ class AttributeSchema(GeneratedAttributeSchema):
117
177
 
118
178
  return data
119
179
 
180
+ def get_regex(self) -> str | None:
181
+ return self.regex
182
+
183
+ def get_min_length(self) -> int | None:
184
+ return self.min_length
185
+
186
+ def get_max_length(self) -> int | None:
187
+ return self.max_length
188
+
120
189
  async def get_query_filter(
121
190
  self,
122
191
  name: str,
@@ -144,3 +213,57 @@ class AttributeSchema(GeneratedAttributeSchema):
144
213
  partial_match=partial_match,
145
214
  support_profiles=support_profiles,
146
215
  )
216
+
217
+
218
+ class NumberPoolSchema(AttributeSchema):
219
+ parameters: NumberPoolParameters = Field(
220
+ default_factory=NumberPoolParameters,
221
+ description="Extra parameters specific to NumberPool attributes",
222
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
223
+ )
224
+
225
+
226
+ class TextAttributeSchema(AttributeSchema):
227
+ parameters: TextAttributeParameters = Field(
228
+ default_factory=TextAttributeParameters,
229
+ description="Extra parameters specific to text attributes",
230
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
231
+ )
232
+
233
+ @model_validator(mode="after")
234
+ def reconcile_parameters(self) -> Self:
235
+ if self.regex != self.parameters.regex:
236
+ final_regex = self.parameters.regex if self.parameters.regex is not None else self.regex
237
+ self.regex = self.parameters.regex = final_regex
238
+ if self.min_length != self.parameters.min_length:
239
+ final_min_length = self.parameters.min_length if self.parameters.min_length is not None else self.min_length
240
+ self.min_length = self.parameters.min_length = final_min_length
241
+ if self.max_length != self.parameters.max_length:
242
+ final_max_length = self.parameters.max_length if self.parameters.max_length is not None else self.max_length
243
+ self.max_length = self.parameters.max_length = final_max_length
244
+ return self
245
+
246
+ def get_regex(self) -> str | None:
247
+ return self.parameters.regex
248
+
249
+ def get_min_length(self) -> int | None:
250
+ return self.parameters.min_length
251
+
252
+ def get_max_length(self) -> int | None:
253
+ return self.parameters.max_length
254
+
255
+
256
+ class NumberAttributeSchema(AttributeSchema):
257
+ parameters: NumberAttributeParameters = Field(
258
+ default_factory=NumberAttributeParameters,
259
+ description="Extra parameters specific to number attributes",
260
+ json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
261
+ )
262
+
263
+
264
+ attribute_schema_class_by_kind: dict[str, type[AttributeSchema]] = {
265
+ "NumberPool": NumberPoolSchema,
266
+ "Text": TextAttributeSchema,
267
+ "TextArea": TextAttributeSchema,
268
+ "Number": NumberAttributeSchema,
269
+ }
@@ -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"}
@@ -362,7 +386,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
362
386
  if not self.display_labels:
363
387
  return None
364
388
 
365
- fields: dict[str, str | None | dict[str, None]] = {}
389
+ fields: dict[str, str | dict[str, None] | None] = {}
366
390
  for item in self.display_labels:
367
391
  fields.update(self.convert_path_to_graphql_fields(path=item))
368
392
  return fields
@@ -377,7 +401,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
377
401
  if not self.human_friendly_id:
378
402
  return None
379
403
 
380
- fields: dict[str, str | None | dict[str, None]] = {}
404
+ fields: dict[str, str | dict[str, None] | None] = {}
381
405
  for item in self.human_friendly_id:
382
406
  fields.update(self.convert_path_to_graphql_fields(path=item))
383
407
  return fields
@@ -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 (
@@ -16,7 +28,13 @@ from .check import core_check_definition
16
28
  from .core import core_node, core_task_target
17
29
  from .generator import core_generator_definition, core_generator_instance
18
30
  from .graphql_query import core_graphql_query
19
- from .group import core_generator_group, core_graphql_query_group, core_group, core_standard_group
31
+ from .group import (
32
+ core_generator_group,
33
+ core_graphql_query_group,
34
+ core_group,
35
+ core_repository_group,
36
+ core_standard_group,
37
+ )
20
38
  from .ipam import builtin_ip_address, builtin_ip_prefix, builtin_ipam, core_ipam_namespace
21
39
  from .lineage import lineage_owner, lineage_source
22
40
  from .menu import generic_menu_item, menu_item
@@ -69,6 +87,9 @@ from .webhook import core_custom_webhook, core_standard_webhook, core_webhook
69
87
 
70
88
  core_models_mixed: dict[str, list] = {
71
89
  "generics": [
90
+ core_action,
91
+ core_trigger_rule,
92
+ core_node_trigger_match,
72
93
  core_node,
73
94
  lineage_owner,
74
95
  core_profile_schema_definition,
@@ -97,12 +118,19 @@ core_models_mixed: dict[str, list] = {
97
118
  ],
98
119
  "nodes": [
99
120
  menu_item,
121
+ core_group_action,
100
122
  core_standard_group,
101
123
  core_generator_group,
102
124
  core_graphql_query_group,
125
+ core_repository_group,
103
126
  builtin_tag,
104
127
  core_account,
105
128
  core_account_token,
129
+ core_generator_action,
130
+ core_group_trigger_rule,
131
+ core_node_trigger_rule,
132
+ core_node_trigger_attribute_match,
133
+ core_node_trigger_relationship_match,
106
134
  core_password_credential,
107
135
  core_refresh_token,
108
136
  core_proposed_change,