infrahub-server 1.2.11__py3-none-any.whl → 1.3.0b1__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 (147) 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/core/account.py +24 -47
  10. infrahub/core/attribute.py +13 -15
  11. infrahub/core/constants/__init__.py +5 -0
  12. infrahub/core/constants/infrahubkind.py +9 -0
  13. infrahub/core/convert_object_type/__init__.py +0 -0
  14. infrahub/core/convert_object_type/conversion.py +122 -0
  15. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  16. infrahub/core/diff/query/all_conflicts.py +1 -5
  17. infrahub/core/diff/query/artifact.py +10 -20
  18. infrahub/core/diff/query/diff_get.py +3 -6
  19. infrahub/core/diff/query/field_summary.py +2 -4
  20. infrahub/core/diff/query/merge.py +70 -123
  21. infrahub/core/diff/query/save.py +20 -32
  22. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  23. infrahub/core/manager.py +14 -11
  24. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  25. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  26. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  27. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  28. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  29. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  30. infrahub/core/migrations/query/attribute_add.py +1 -2
  31. infrahub/core/migrations/query/attribute_rename.py +5 -10
  32. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  33. infrahub/core/migrations/query/node_duplicate.py +19 -21
  34. infrahub/core/migrations/query/relationship_duplicate.py +19 -17
  35. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  36. infrahub/core/migrations/schema/node_remove.py +19 -19
  37. infrahub/core/models.py +29 -2
  38. infrahub/core/node/__init__.py +90 -18
  39. infrahub/core/node/create.py +211 -0
  40. infrahub/core/node/resource_manager/number_pool.py +31 -5
  41. infrahub/core/node/standard.py +6 -1
  42. infrahub/core/protocols.py +56 -0
  43. infrahub/core/protocols_base.py +3 -0
  44. infrahub/core/query/__init__.py +2 -2
  45. infrahub/core/query/diff.py +19 -32
  46. infrahub/core/query/ipam.py +10 -20
  47. infrahub/core/query/node.py +28 -46
  48. infrahub/core/query/relationship.py +53 -32
  49. infrahub/core/query/resource_manager.py +1 -2
  50. infrahub/core/query/subquery.py +2 -4
  51. infrahub/core/relationship/model.py +3 -0
  52. infrahub/core/schema/__init__.py +2 -1
  53. infrahub/core/schema/attribute_parameters.py +160 -0
  54. infrahub/core/schema/attribute_schema.py +111 -8
  55. infrahub/core/schema/basenode_schema.py +25 -1
  56. infrahub/core/schema/definitions/core/__init__.py +29 -1
  57. infrahub/core/schema/definitions/core/group.py +45 -0
  58. infrahub/core/schema/definitions/internal.py +27 -4
  59. infrahub/core/schema/generated/attribute_schema.py +16 -3
  60. infrahub/core/schema/manager.py +3 -0
  61. infrahub/core/schema/schema_branch.py +67 -7
  62. infrahub/core/validators/__init__.py +13 -1
  63. infrahub/core/validators/attribute/choices.py +1 -3
  64. infrahub/core/validators/attribute/enum.py +1 -3
  65. infrahub/core/validators/attribute/kind.py +1 -3
  66. infrahub/core/validators/attribute/length.py +13 -7
  67. infrahub/core/validators/attribute/min_max.py +118 -0
  68. infrahub/core/validators/attribute/number_pool.py +106 -0
  69. infrahub/core/validators/attribute/optional.py +1 -4
  70. infrahub/core/validators/attribute/regex.py +5 -6
  71. infrahub/core/validators/attribute/unique.py +1 -3
  72. infrahub/core/validators/determiner.py +18 -2
  73. infrahub/core/validators/enum.py +12 -0
  74. infrahub/core/validators/node/hierarchy.py +3 -6
  75. infrahub/core/validators/query.py +1 -3
  76. infrahub/core/validators/relationship/count.py +6 -12
  77. infrahub/core/validators/relationship/optional.py +2 -4
  78. infrahub/core/validators/relationship/peer.py +3 -8
  79. infrahub/core/validators/uniqueness/query.py +5 -9
  80. infrahub/database/__init__.py +11 -2
  81. infrahub/events/group_action.py +1 -0
  82. infrahub/git/base.py +5 -3
  83. infrahub/git/integrator.py +102 -3
  84. infrahub/graphql/analyzer.py +139 -18
  85. infrahub/graphql/manager.py +4 -0
  86. infrahub/graphql/mutations/action.py +164 -0
  87. infrahub/graphql/mutations/convert_object_type.py +62 -0
  88. infrahub/graphql/mutations/main.py +24 -175
  89. infrahub/graphql/mutations/proposed_change.py +20 -17
  90. infrahub/graphql/mutations/resource_manager.py +62 -6
  91. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  92. infrahub/graphql/queries/resource_manager.py +7 -1
  93. infrahub/graphql/schema.py +6 -0
  94. infrahub/menu/menu.py +31 -0
  95. infrahub/message_bus/messages/__init__.py +0 -10
  96. infrahub/message_bus/operations/__init__.py +0 -8
  97. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  98. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  99. infrahub/pools/number.py +5 -3
  100. infrahub/prefect_server/models.py +1 -19
  101. infrahub/proposed_change/models.py +68 -3
  102. infrahub/proposed_change/tasks.py +907 -30
  103. infrahub/task_manager/models.py +10 -6
  104. infrahub/trigger/catalogue.py +2 -0
  105. infrahub/trigger/models.py +18 -2
  106. infrahub/trigger/tasks.py +3 -1
  107. infrahub/types.py +6 -0
  108. infrahub/workflows/catalogue.py +76 -0
  109. infrahub_sdk/client.py +43 -10
  110. infrahub_sdk/node/__init__.py +39 -0
  111. infrahub_sdk/node/attribute.py +122 -0
  112. infrahub_sdk/node/constants.py +21 -0
  113. infrahub_sdk/{node.py → node/node.py} +50 -749
  114. infrahub_sdk/node/parsers.py +15 -0
  115. infrahub_sdk/node/property.py +24 -0
  116. infrahub_sdk/node/related_node.py +266 -0
  117. infrahub_sdk/node/relationship.py +302 -0
  118. infrahub_sdk/protocols.py +112 -0
  119. infrahub_sdk/protocols_base.py +34 -2
  120. infrahub_sdk/query_groups.py +13 -2
  121. infrahub_sdk/schema/main.py +1 -0
  122. infrahub_sdk/schema/repository.py +16 -0
  123. infrahub_sdk/spec/object.py +1 -1
  124. infrahub_sdk/store.py +1 -1
  125. infrahub_sdk/testing/schemas/car_person.py +1 -0
  126. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/METADATA +4 -4
  127. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/RECORD +134 -122
  128. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/WHEEL +1 -1
  129. infrahub_testcontainers/container.py +0 -1
  130. infrahub_testcontainers/docker-compose.test.yml +1 -1
  131. infrahub_testcontainers/helpers.py +8 -2
  132. infrahub/message_bus/messages/check_generator_run.py +0 -26
  133. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  134. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  135. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  136. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  137. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  138. infrahub/message_bus/operations/check/__init__.py +0 -3
  139. infrahub/message_bus/operations/check/generator.py +0 -156
  140. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  141. infrahub/message_bus/operations/finalize/validator.py +0 -133
  142. infrahub/message_bus/operations/requests/__init__.py +0 -9
  143. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  144. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  145. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  146. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/LICENSE.txt +0 -0
  147. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/entry_points.txt +0 -0
@@ -22,7 +22,15 @@ from infrahub.core.constants import (
22
22
  from infrahub.core.constants.schema import SchemaElementPathType
23
23
  from infrahub.core.protocols import CoreNumberPool, CoreObjectTemplate
24
24
  from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
25
- from infrahub.core.schema import AttributeSchema, NodeSchema, ProfileSchema, RelationshipSchema, TemplateSchema
25
+ from infrahub.core.schema import (
26
+ AttributeSchema,
27
+ NodeSchema,
28
+ NonGenericSchemaTypes,
29
+ ProfileSchema,
30
+ RelationshipSchema,
31
+ TemplateSchema,
32
+ )
33
+ from infrahub.core.schema.attribute_parameters import NumberPoolParameters
26
34
  from infrahub.core.timestamp import Timestamp
27
35
  from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
28
36
  from infrahub.types import ATTRIBUTE_TYPES
@@ -66,7 +74,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
66
74
  _meta.default_filter = default_filter
67
75
  super().__init_subclass_with_meta__(_meta=_meta, **options)
68
76
 
69
- def get_schema(self) -> NodeSchema | ProfileSchema | TemplateSchema:
77
+ def get_schema(self) -> NonGenericSchemaTypes:
70
78
  return self._schema
71
79
 
72
80
  def get_kind(self) -> str:
@@ -247,6 +255,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
247
255
  within the create code.
248
256
  """
249
257
 
258
+ number_pool_parameters: NumberPoolParameters | None = None
259
+ if attribute.schema.kind == "NumberPool" and isinstance(attribute.schema.parameters, NumberPoolParameters):
260
+ attribute.from_pool = {"id": attribute.schema.parameters.number_pool_id}
261
+ attribute.is_default = False
262
+ number_pool_parameters = attribute.schema.parameters
263
+
250
264
  if not attribute.from_pool:
251
265
  return
252
266
 
@@ -255,19 +269,25 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
255
269
  db=db, id=attribute.from_pool["id"], kind=CoreNumberPool
256
270
  )
257
271
  except NodeNotFoundError:
258
- errors.append(
259
- ValidationError(
260
- {f"{attribute.name}.from_pool": f"The pool requested {attribute.from_pool} was not found."}
272
+ if number_pool_parameters:
273
+ number_pool = await self._create_number_pool(
274
+ db=db, attribute=attribute, number_pool_parameters=number_pool_parameters
261
275
  )
262
- )
263
- return
276
+
277
+ else:
278
+ errors.append(
279
+ ValidationError(
280
+ {f"{attribute.name}.from_pool": f"The pool requested {attribute.from_pool} was not found."}
281
+ )
282
+ )
283
+ return
264
284
 
265
285
  if (
266
286
  number_pool.node.value in [self._schema.kind] + self._schema.inherit_from
267
287
  and number_pool.node_attribute.value == attribute.name
268
288
  ):
269
289
  try:
270
- next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self)
290
+ next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self, attribute=attribute)
271
291
  except PoolExhaustedError:
272
292
  errors.append(
273
293
  ValidationError({f"{attribute.name}.from_pool": f"The pool {number_pool.node.value} is exhausted."})
@@ -285,6 +305,35 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
285
305
  )
286
306
  )
287
307
 
308
+ async def _create_number_pool(
309
+ self, db: InfrahubDatabase, attribute: BaseAttribute, number_pool_parameters: NumberPoolParameters
310
+ ) -> CoreNumberPool:
311
+ schema = db.schema.get_node_schema(name="CoreNumberPool", duplicate=False)
312
+
313
+ pool_node = self._schema.kind
314
+ schema_attribute = self._schema.get_attribute(attribute.schema.name)
315
+ if schema_attribute.inherited:
316
+ for generic_name in self._schema.inherit_from:
317
+ generic_node = db.schema.get_generic_schema(name=generic_name, duplicate=False)
318
+ if attribute.schema.name in generic_node.attribute_names:
319
+ pool_node = generic_node.kind
320
+ break
321
+
322
+ number_pool = await Node.init(db=db, schema=schema, branch=self._branch)
323
+ await number_pool.new(
324
+ db=db,
325
+ id=number_pool_parameters.number_pool_id,
326
+ name=f"{pool_node}.{attribute.schema.name} [{number_pool_parameters.number_pool_id}]",
327
+ node=pool_node,
328
+ node_attribute=attribute.schema.name,
329
+ start_range=number_pool_parameters.start_range,
330
+ end_range=number_pool_parameters.end_range,
331
+ )
332
+ await number_pool.save(db=db)
333
+ # Do a lookup of the number pool to get the correct mapped type from the registry
334
+ # without this we don't get access to the .get_resource() method.
335
+ return await registry.manager.get_one_by_id_or_default_filter(db=db, id=number_pool.id, kind=CoreNumberPool)
336
+
288
337
  async def handle_object_template(self, fields: dict, db: InfrahubDatabase, errors: list) -> None:
289
338
  """Fill the `fields` parameters with values from an object template if one is in use."""
290
339
  object_template_field = fields.get(OBJECT_TEMPLATE_RELATIONSHIP_NAME)
@@ -369,6 +418,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
369
418
  self._computed_jinja2_attributes.append(mandatory_attr)
370
419
  continue
371
420
 
421
+ if mandatory_attribute.kind == "NumberPool":
422
+ continue
423
+
372
424
  errors.append(
373
425
  ValidationError({mandatory_attr: f"{mandatory_attr} is mandatory for {self.get_kind()}"})
374
426
  )
@@ -385,6 +437,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
385
437
  # -------------------------------------------
386
438
  # Generate Attribute and Relationship and assign them
387
439
  # -------------------------------------------
440
+ errors.extend(await self._process_fields_relationships(fields=fields, db=db))
441
+ errors.extend(await self._process_fields_attributes(fields=fields, db=db))
442
+
443
+ if errors:
444
+ raise ValidationError(errors)
445
+
446
+ # Check if any post processor have been defined
447
+ # A processor can be used for example to assigne a default value
448
+ for name in self._attributes + self._relationships:
449
+ if hasattr(self, f"process_{name}"):
450
+ await getattr(self, f"process_{name}")(db=db)
451
+
452
+ async def _process_fields_relationships(self, fields: dict, db: InfrahubDatabase) -> list[ValidationError]:
453
+ errors: list[ValidationError] = []
454
+
388
455
  for rel_schema in self._schema.relationships:
389
456
  self._relationships.append(rel_schema.name)
390
457
 
@@ -406,6 +473,11 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
406
473
  except ValidationError as exc:
407
474
  errors.append(exc)
408
475
 
476
+ return errors
477
+
478
+ async def _process_fields_attributes(self, fields: dict, db: InfrahubDatabase) -> list[ValidationError]:
479
+ errors: list[ValidationError] = []
480
+
409
481
  for attr_schema in self._schema.attributes:
410
482
  self._attributes.append(attr_schema.name)
411
483
  if not self._existing and attr_schema.name in self._computed_jinja2_attributes:
@@ -434,14 +506,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
434
506
  except ValidationError as exc:
435
507
  errors.append(exc)
436
508
 
437
- if errors:
438
- raise ValidationError(errors)
439
-
440
- # Check if any post processor have been defined
441
- # A processor can be used for example to assigne a default value
442
- for name in self._attributes + self._relationships:
443
- if hasattr(self, f"process_{name}"):
444
- await getattr(self, f"process_{name}")(db=db)
509
+ return errors
445
510
 
446
511
  async def _process_macros(self, db: InfrahubDatabase) -> None:
447
512
  schema_branch = db.schema.get_schema_branch(self._branch.name)
@@ -872,9 +937,11 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
872
937
  if relationship.kind == RelationshipKind.PARENT:
873
938
  return relationship.name
874
939
 
875
- async def get_object_template(self, db: InfrahubDatabase) -> Node | None:
940
+ async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
876
941
  object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
877
- return await object_template.get_peer(db=db) if object_template is not None else None
942
+ return (
943
+ await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
944
+ )
878
945
 
879
946
  def get_relationships(
880
947
  self, kind: RelationshipKind, exclude: Sequence[str] | None = None
@@ -888,3 +955,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
888
955
  for relationship in self.get_schema().relationships
889
956
  if relationship.name not in exclude and relationship.kind == kind
890
957
  ]
958
+
959
+ def validate_relationships(self) -> None:
960
+ for name in self._relationships:
961
+ relm: RelationshipManager = getattr(self, name)
962
+ relm.validate()
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Mapping
4
+
5
+ from infrahub.core import registry
6
+ from infrahub.core.constants import RelationshipCardinality, RelationshipKind
7
+ from infrahub.core.constraint.node.runner import NodeConstraintRunner
8
+ from infrahub.core.manager import NodeManager
9
+ from infrahub.core.node import Node
10
+ from infrahub.core.protocols import CoreObjectTemplate
11
+ from infrahub.dependencies.registry import get_component_registry
12
+
13
+ if TYPE_CHECKING:
14
+ from infrahub.core.branch import Branch
15
+ from infrahub.core.relationship.model import RelationshipManager
16
+ from infrahub.core.schema import MainSchemaTypes, NonGenericSchemaTypes, RelationshipSchema
17
+ from infrahub.database import InfrahubDatabase
18
+
19
+
20
+ async def get_template_relationship_peers(
21
+ db: InfrahubDatabase, template: CoreObjectTemplate, relationship: RelationshipSchema
22
+ ) -> Mapping[str, CoreObjectTemplate]:
23
+ """For a given relationship on the template, fetch the related peers."""
24
+ template_relationship_manager: RelationshipManager = getattr(template, relationship.name)
25
+ if relationship.cardinality == RelationshipCardinality.MANY:
26
+ return await template_relationship_manager.get_peers(db=db, peer_type=CoreObjectTemplate)
27
+
28
+ peers: dict[str, CoreObjectTemplate] = {}
29
+ template_relationship_peer = await template_relationship_manager.get_peer(db=db, peer_type=CoreObjectTemplate)
30
+ if template_relationship_peer:
31
+ peers[template_relationship_peer.id] = template_relationship_peer
32
+ return peers
33
+
34
+
35
+ async def extract_peer_data(
36
+ db: InfrahubDatabase,
37
+ template_peer: CoreObjectTemplate,
38
+ obj_peer_schema: MainSchemaTypes,
39
+ parent_obj: Node,
40
+ current_template: CoreObjectTemplate,
41
+ ) -> Mapping[str, Any]:
42
+ obj_peer_data: dict[str, Any] = {}
43
+
44
+ for attr in template_peer.get_schema().attribute_names:
45
+ if attr not in obj_peer_schema.attribute_names:
46
+ continue
47
+ obj_peer_data[attr] = {"value": getattr(template_peer, attr).value, "source": template_peer.id}
48
+
49
+ for rel in template_peer.get_schema().relationship_names:
50
+ rel_manager: RelationshipManager = getattr(template_peer, rel)
51
+ if (
52
+ rel_manager.schema.kind not in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
53
+ or rel_manager.schema.name not in obj_peer_schema.relationship_names
54
+ ):
55
+ continue
56
+
57
+ if list(await rel_manager.get_peers(db=db)) == [current_template.id]:
58
+ obj_peer_data[rel] = {"id": parent_obj.id}
59
+
60
+ return obj_peer_data
61
+
62
+
63
+ async def handle_template_relationships(
64
+ db: InfrahubDatabase,
65
+ branch: Branch,
66
+ obj: Node,
67
+ template: CoreObjectTemplate,
68
+ fields: list,
69
+ constraint_runner: NodeConstraintRunner | None = None,
70
+ ) -> None:
71
+ if constraint_runner is None:
72
+ component_registry = get_component_registry()
73
+ constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
74
+
75
+ for relationship in obj.get_relationships(kind=RelationshipKind.COMPONENT, exclude=fields):
76
+ template_relationship_peers = await get_template_relationship_peers(
77
+ db=db, template=template, relationship=relationship
78
+ )
79
+ if not template_relationship_peers:
80
+ continue
81
+
82
+ for template_relationship_peer in template_relationship_peers.values():
83
+ # We retrieve peer schema for each peer in case we are processing a relationship which is based on a generic
84
+ obj_peer_schema = registry.schema.get_node_schema(
85
+ name=template_relationship_peer.get_schema().kind.removeprefix("Template"),
86
+ branch=branch,
87
+ duplicate=False,
88
+ )
89
+ obj_peer_data = await extract_peer_data(
90
+ db=db,
91
+ template_peer=template_relationship_peer,
92
+ obj_peer_schema=obj_peer_schema,
93
+ parent_obj=obj,
94
+ current_template=template,
95
+ )
96
+
97
+ obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch)
98
+ await obj_peer.new(db=db, **obj_peer_data)
99
+ await constraint_runner.check(node=obj_peer, field_filters=list(obj_peer_data))
100
+ await obj_peer.save(db=db)
101
+
102
+ await handle_template_relationships(
103
+ db=db,
104
+ branch=branch,
105
+ constraint_runner=constraint_runner,
106
+ obj=obj_peer,
107
+ template=template_relationship_peer,
108
+ fields=fields,
109
+ )
110
+
111
+
112
+ async def get_profile_ids(db: InfrahubDatabase, obj: Node) -> set[str]:
113
+ if not hasattr(obj, "profiles"):
114
+ return set()
115
+ profile_rels = await obj.profiles.get_relationships(db=db)
116
+ return {pr.peer_id for pr in profile_rels}
117
+
118
+
119
+ async def refresh_for_profile_update(
120
+ db: InfrahubDatabase,
121
+ branch: Branch,
122
+ obj: Node,
123
+ schema: NonGenericSchemaTypes,
124
+ previous_profile_ids: set[str] | None = None,
125
+ ) -> Node:
126
+ if not hasattr(obj, "profiles"):
127
+ return obj
128
+ current_profile_ids = await get_profile_ids(db=db, obj=obj)
129
+ if previous_profile_ids is None or previous_profile_ids != current_profile_ids:
130
+ refreshed_node = await NodeManager.get_one_by_id_or_default_filter(
131
+ db=db,
132
+ kind=schema.kind,
133
+ id=obj.get_id(),
134
+ branch=branch,
135
+ include_owner=True,
136
+ include_source=True,
137
+ )
138
+ refreshed_node._node_changelog = obj.node_changelog
139
+ return refreshed_node
140
+ return obj
141
+
142
+
143
+ async def _do_create_node(
144
+ node_class: type[Node],
145
+ db: InfrahubDatabase,
146
+ data: dict,
147
+ schema: NonGenericSchemaTypes,
148
+ fields_to_validate: list,
149
+ branch: Branch,
150
+ node_constraint_runner: NodeConstraintRunner,
151
+ ) -> Node:
152
+ obj = await node_class.init(db=db, schema=schema, branch=branch)
153
+ await obj.new(db=db, **data)
154
+ await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
155
+ await obj.save(db=db)
156
+
157
+ object_template = await obj.get_object_template(db=db)
158
+ if object_template:
159
+ await handle_template_relationships(
160
+ db=db,
161
+ branch=branch,
162
+ template=object_template,
163
+ obj=obj,
164
+ fields=fields_to_validate,
165
+ )
166
+ return obj
167
+
168
+
169
+ async def create_node(
170
+ data: dict,
171
+ db: InfrahubDatabase,
172
+ branch: Branch,
173
+ schema: NonGenericSchemaTypes,
174
+ ) -> Node:
175
+ """Create a node in the database if constraint checks succeed."""
176
+
177
+ component_registry = get_component_registry()
178
+ node_constraint_runner = await component_registry.get_component(
179
+ NodeConstraintRunner, db=db.start_session() if not db.is_transaction else db, branch=branch
180
+ )
181
+ node_class = Node
182
+ if schema.kind in registry.node:
183
+ node_class = registry.node[schema.kind]
184
+
185
+ fields_to_validate = list(data)
186
+ if db.is_transaction:
187
+ obj = await _do_create_node(
188
+ node_class=node_class,
189
+ node_constraint_runner=node_constraint_runner,
190
+ db=db,
191
+ schema=schema,
192
+ branch=branch,
193
+ fields_to_validate=fields_to_validate,
194
+ data=data,
195
+ )
196
+ else:
197
+ async with db.start_transaction() as dbt:
198
+ obj = await _do_create_node(
199
+ node_class=node_class,
200
+ node_constraint_runner=node_constraint_runner,
201
+ db=dbt,
202
+ schema=schema,
203
+ branch=branch,
204
+ fields_to_validate=fields_to_validate,
205
+ data=data,
206
+ )
207
+
208
+ if await get_profile_ids(db=db, obj=obj):
209
+ obj = await refresh_for_profile_update(db=db, branch=branch, schema=schema, obj=obj)
210
+
211
+ return obj
@@ -2,22 +2,44 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from infrahub.core import registry
5
6
  from infrahub.core.query.resource_manager import NumberPoolGetReserved, NumberPoolGetUsed, NumberPoolSetReserved
7
+ from infrahub.core.schema.attribute_parameters import NumberAttributeParameters
6
8
  from infrahub.exceptions import PoolExhaustedError
7
9
 
8
10
  from .. import Node
9
11
 
10
12
  if TYPE_CHECKING:
13
+ from infrahub.core.attribute import BaseAttribute
11
14
  from infrahub.core.branch import Branch
12
15
  from infrahub.database import InfrahubDatabase
13
16
 
14
17
 
15
18
  class CoreNumberPool(Node):
19
+ def get_attribute_nb_excluded_values(self) -> int:
20
+ """
21
+ Returns the number of excluded values for the attribute of the number pool.
22
+ """
23
+
24
+ pool_node = registry.schema.get(name=self.node.value) # type: ignore [attr-defined]
25
+ attribute = [attribute for attribute in pool_node.attributes if attribute.name == self.node_attribute.value][0] # type: ignore [attr-defined]
26
+ if not isinstance(attribute.parameters, NumberAttributeParameters):
27
+ return 0
28
+
29
+ sum_excluded_values = 0
30
+ excluded_ranges = attribute.parameters.get_excluded_ranges()
31
+ for start_range, end_range in excluded_ranges:
32
+ sum_excluded_values += end_range - start_range + 1
33
+
34
+ res = len(attribute.parameters.get_excluded_single_values()) + sum_excluded_values
35
+ return res
36
+
16
37
  async def get_resource(
17
38
  self,
18
39
  db: InfrahubDatabase,
19
40
  branch: Branch,
20
41
  node: Node,
42
+ attribute: BaseAttribute,
21
43
  identifier: str | None = None,
22
44
  ) -> int:
23
45
  identifier = identifier or node.get_id()
@@ -31,23 +53,24 @@ class CoreNumberPool(Node):
31
53
  return reservation
32
54
 
33
55
  # If we have not returned a value we need to find one if avaiable
34
- number = await self.get_next(db=db, branch=branch)
56
+ number = await self.get_next(db=db, branch=branch, attribute=attribute)
35
57
 
36
58
  query_set = await NumberPoolSetReserved.init(
37
59
  db=db, pool_id=self.get_id(), identifier=identifier, reserved=number
38
60
  )
39
61
  await query_set.execute(db=db)
40
-
41
62
  return number
42
63
 
43
- async def get_next(self, db: InfrahubDatabase, branch: Branch) -> int:
64
+ async def get_next(self, db: InfrahubDatabase, branch: Branch, attribute: BaseAttribute) -> int:
44
65
  query = await NumberPoolGetUsed.init(db=db, branch=branch, pool=self, branch_agnostic=True)
45
66
  await query.execute(db=db)
46
67
  taken = [result.get_as_optional_type("av.value", return_type=int) for result in query.results]
68
+ parameters = attribute.schema.parameters
47
69
  next_number = find_next_free(
48
70
  start=self.start_range.value, # type: ignore[attr-defined]
49
71
  end=self.end_range.value, # type: ignore[attr-defined]
50
72
  taken=taken,
73
+ parameters=parameters if isinstance(parameters, NumberAttributeParameters) else None,
51
74
  )
52
75
  if next_number is None:
53
76
  raise PoolExhaustedError("There are no more values available in this pool.")
@@ -55,12 +78,15 @@ class CoreNumberPool(Node):
55
78
  return next_number
56
79
 
57
80
 
58
- def find_next_free(start: int, end: int, taken: list[int | None]) -> int | None:
81
+ def find_next_free(
82
+ start: int, end: int, taken: list[int | None], parameters: NumberAttributeParameters | None
83
+ ) -> int | None:
59
84
  used_numbers = [number for number in taken if number is not None]
60
85
  used_set = set(used_numbers)
61
86
 
62
87
  for num in range(start, end + 1):
63
88
  if num not in used_set:
64
- return num
89
+ if parameters is None or parameters.is_valid_value(num):
90
+ return num
65
91
 
66
92
  return None
@@ -210,7 +210,12 @@ class StandardNode(BaseModel):
210
210
 
211
211
  @classmethod
212
212
  async def get_list(
213
- cls, db: InfrahubDatabase, limit: int = 1000, ids: list[str] | None = None, name: str | None = None, **kwargs
213
+ cls,
214
+ db: InfrahubDatabase,
215
+ limit: int = 1000,
216
+ ids: list[str] | None = None,
217
+ name: str | None = None,
218
+ **kwargs: dict[str, Any],
214
219
  ) -> list[Self]:
215
220
  query: Query = await StandardNodeGetListQuery.init(
216
221
  db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs
@@ -62,6 +62,12 @@ class BuiltinIPPrefix(CoreNode):
62
62
  children: RelationshipManager
63
63
 
64
64
 
65
+ class CoreAction(CoreNode):
66
+ name: String
67
+ description: StringOptional
68
+ triggers: RelationshipManager
69
+
70
+
65
71
  class CoreArtifactTarget(CoreNode):
66
72
  artifacts: RelationshipManager
67
73
 
@@ -148,6 +154,10 @@ class CoreMenu(CoreNode):
148
154
  children: RelationshipManager
149
155
 
150
156
 
157
+ class CoreNodeTriggerMatch(CoreNode):
158
+ trigger: RelationshipManager
159
+
160
+
151
161
  class CoreObjectComponentTemplate(CoreNode):
152
162
  template_name: String
153
163
 
@@ -189,6 +199,14 @@ class CoreTransformation(CoreNode):
189
199
  tags: RelationshipManager
190
200
 
191
201
 
202
+ class CoreTriggerRule(CoreNode):
203
+ name: String
204
+ description: StringOptional
205
+ active: Boolean
206
+ branch_scope: Dropdown
207
+ action: RelationshipManager
208
+
209
+
192
210
  class CoreValidator(CoreNode):
193
211
  label: StringOptional
194
212
  state: Enum
@@ -326,6 +344,10 @@ class CoreFileThread(CoreThread):
326
344
  repository: RelationshipManager
327
345
 
328
346
 
347
+ class CoreGeneratorAction(CoreAction):
348
+ generator: RelationshipManager
349
+
350
+
329
351
  class CoreGeneratorCheck(CoreCheck):
330
352
  instance: String
331
353
 
@@ -380,6 +402,16 @@ class CoreGraphQLQueryGroup(CoreGroup):
380
402
  query: RelationshipManager
381
403
 
382
404
 
405
+ class CoreGroupAction(CoreAction):
406
+ add_members: Boolean
407
+ group: RelationshipManager
408
+
409
+
410
+ class CoreGroupTriggerRule(CoreTriggerRule):
411
+ members_added: Boolean
412
+ group: RelationshipManager
413
+
414
+
383
415
  class CoreIPAddressPool(CoreResourcePool, LineageSource):
384
416
  default_address_type: String
385
417
  default_prefix_length: IntegerOptional
@@ -399,6 +431,25 @@ class CoreMenuItem(CoreMenu):
399
431
  pass
400
432
 
401
433
 
434
+ class CoreNodeTriggerAttributeMatch(CoreNodeTriggerMatch):
435
+ attribute_name: String
436
+ value: StringOptional
437
+ value_previous: StringOptional
438
+ value_match: Dropdown
439
+
440
+
441
+ class CoreNodeTriggerRelationshipMatch(CoreNodeTriggerMatch):
442
+ relationship_name: String
443
+ added: Boolean
444
+ peer: StringOptional
445
+
446
+
447
+ class CoreNodeTriggerRule(CoreTriggerRule):
448
+ node_kind: String
449
+ mutation_action: Enum
450
+ matches: RelationshipManager
451
+
452
+
402
453
  class CoreNumberPool(CoreResourcePool, LineageSource):
403
454
  node: String
404
455
  node_attribute: String
@@ -446,6 +497,11 @@ class CoreRepository(LineageOwner, LineageSource, CoreGenericRepository, CoreTas
446
497
  commit: StringOptional
447
498
 
448
499
 
500
+ class CoreRepositoryGroup(CoreGroup):
501
+ content: Dropdown
502
+ repository: RelationshipManager
503
+
504
+
449
505
  class CoreRepositoryValidator(CoreValidator):
450
506
  repository: RelationshipManager
451
507
 
@@ -7,6 +7,7 @@ from typing_extensions import Self
7
7
  if TYPE_CHECKING:
8
8
  from neo4j import AsyncResult, AsyncSession, AsyncTransaction, Record
9
9
 
10
+ from infrahub.core.schema import NonGenericSchemaTypes
10
11
  from infrahub.core.schema.schema_branch import SchemaBranch
11
12
 
12
13
 
@@ -70,6 +71,8 @@ class CoreNode(Protocol):
70
71
 
71
72
  def get_id(self) -> str: ...
72
73
  def get_kind(self) -> str: ...
74
+ def get_schema(self) -> NonGenericSchemaTypes: ...
75
+
73
76
  @classmethod
74
77
  async def init(
75
78
  cls,
@@ -424,8 +424,8 @@ class Query(ABC):
424
424
  else:
425
425
  self.query_lines.extend([line.strip() for line in query.split("\n") if line.strip()])
426
426
 
427
- def add_subquery(self, subquery: str, with_clause: str | None = None) -> None:
428
- self.add_to_query("CALL {")
427
+ def add_subquery(self, subquery: str, node_alias: str, with_clause: str | None = None) -> None:
428
+ self.add_to_query(f"CALL ({node_alias}) {{")
429
429
  self.add_to_query(subquery)
430
430
  self.add_to_query("}")
431
431
  if with_clause: