infrahub-server 1.2.3__py3-none-any.whl → 1.2.5__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 (63) hide show
  1. infrahub/cli/db.py +308 -2
  2. infrahub/cli/git_agent.py +4 -10
  3. infrahub/config.py +32 -0
  4. infrahub/core/branch/tasks.py +50 -10
  5. infrahub/core/constants/__init__.py +1 -0
  6. infrahub/core/constraint/node/runner.py +6 -5
  7. infrahub/core/graph/__init__.py +1 -1
  8. infrahub/core/migrations/graph/__init__.py +4 -0
  9. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +68 -70
  10. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +26 -0
  11. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +54 -0
  12. infrahub/core/migrations/schema/node_attribute_remove.py +16 -2
  13. infrahub/core/models.py +1 -1
  14. infrahub/core/node/__init__.py +4 -1
  15. infrahub/core/node/constraints/grouped_uniqueness.py +6 -1
  16. infrahub/core/node/resource_manager/number_pool.py +1 -1
  17. infrahub/core/registry.py +18 -0
  18. infrahub/core/schema/basenode_schema.py +21 -1
  19. infrahub/core/schema/definitions/internal.py +2 -1
  20. infrahub/core/schema/generated/base_node_schema.py +1 -1
  21. infrahub/core/schema/manager.py +21 -1
  22. infrahub/core/schema/schema_branch.py +8 -7
  23. infrahub/core/schema/schema_branch_computed.py +12 -1
  24. infrahub/database/__init__.py +10 -0
  25. infrahub/events/branch_action.py +3 -0
  26. infrahub/events/group_action.py +6 -1
  27. infrahub/events/node_action.py +5 -1
  28. infrahub/git/integrator.py +2 -2
  29. infrahub/graphql/mutations/main.py +10 -12
  30. infrahub/message_bus/messages/__init__.py +0 -4
  31. infrahub/message_bus/messages/request_proposedchange_pipeline.py +5 -0
  32. infrahub/message_bus/operations/__init__.py +0 -3
  33. infrahub/message_bus/operations/requests/proposed_change.py +29 -9
  34. infrahub/message_bus/types.py +2 -34
  35. infrahub/proposed_change/branch_diff.py +65 -0
  36. infrahub/proposed_change/tasks.py +12 -4
  37. infrahub/server.py +6 -11
  38. infrahub/services/adapters/cache/__init__.py +17 -0
  39. infrahub/services/adapters/cache/redis.py +11 -1
  40. infrahub/services/adapters/message_bus/__init__.py +20 -0
  41. infrahub/services/adapters/workflow/worker.py +1 -1
  42. infrahub/services/component.py +1 -2
  43. infrahub/tasks/registry.py +3 -7
  44. infrahub/workers/infrahub_async.py +4 -10
  45. infrahub/workflows/catalogue.py +10 -0
  46. infrahub_sdk/generator.py +1 -0
  47. infrahub_sdk/node.py +16 -4
  48. infrahub_sdk/schema/__init__.py +10 -1
  49. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/METADATA +2 -2
  50. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/RECORD +57 -60
  51. infrahub_testcontainers/container.py +4 -0
  52. infrahub_testcontainers/helpers.py +1 -1
  53. infrahub_testcontainers/models.py +2 -2
  54. infrahub_testcontainers/performance_test.py +4 -4
  55. infrahub/core/branch/flow_models.py +0 -0
  56. infrahub/message_bus/messages/event_branch_merge.py +0 -13
  57. infrahub/message_bus/messages/event_worker_newprimaryapi.py +0 -9
  58. infrahub/message_bus/operations/event/__init__.py +0 -3
  59. infrahub/message_bus/operations/event/branch.py +0 -61
  60. infrahub/message_bus/operations/event/worker.py +0 -9
  61. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/LICENSE.txt +0 -0
  62. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/WHEEL +0 -0
  63. {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/entry_points.txt +0 -0
@@ -21,81 +21,79 @@ if TYPE_CHECKING:
21
21
  log = get_logger()
22
22
 
23
23
 
24
+ async def validate_nulls_in_uniqueness_constraints(db: InfrahubDatabase) -> MigrationResult:
25
+ """
26
+ Validate any schema that include optional attributes in the uniqueness constraints
27
+
28
+ An update to uniqueness constraint validation now handles NULL values as unique instead of ignoring them
29
+ """
30
+
31
+ default_branch = registry.get_branch_from_registry()
32
+ build_component_registry()
33
+ component_registry = get_component_registry()
34
+ uniqueness_checker = await component_registry.get_component(UniquenessChecker, db=db, branch=default_branch)
35
+ non_unique_nodes_by_kind: dict[str, list[NonUniqueNode]] = defaultdict(list)
36
+
37
+ manager = SchemaManager()
38
+ registry.schema = manager
39
+ internal_schema_root = SchemaRoot(**internal_schema)
40
+ manager.register_schema(schema=internal_schema_root)
41
+ schema_branch = await manager.load_schema_from_db(db=db, branch=default_branch)
42
+ manager.set_schema_branch(name=default_branch.name, schema=schema_branch)
43
+
44
+ for schema_kind in schema_branch.node_names + schema_branch.generic_names_without_templates:
45
+ schema = schema_branch.get(name=schema_kind, duplicate=False)
46
+ if not isinstance(schema, NodeSchema | GenericSchema):
47
+ continue
48
+
49
+ uniqueness_constraint_paths = schema.get_unique_constraint_schema_attribute_paths(schema_branch=schema_branch)
50
+ includes_optional_attr: bool = False
51
+
52
+ for uniqueness_constraint_path in uniqueness_constraint_paths:
53
+ for schema_attribute_path in uniqueness_constraint_path.attributes_paths:
54
+ if schema_attribute_path.attribute_schema and schema_attribute_path.attribute_schema.optional is True:
55
+ includes_optional_attr = True
56
+ break
57
+
58
+ if not includes_optional_attr:
59
+ continue
60
+
61
+ non_unique_nodes = await uniqueness_checker.check_one_schema(schema=schema)
62
+ if non_unique_nodes:
63
+ non_unique_nodes_by_kind[schema_kind] = non_unique_nodes
64
+
65
+ if not non_unique_nodes_by_kind:
66
+ return MigrationResult()
67
+
68
+ error_strings = []
69
+ for schema_kind, non_unique_nodes in non_unique_nodes_by_kind.items():
70
+ display_label_map = await get_display_labels_per_kind(
71
+ db=db, kind=schema_kind, branch_name=default_branch.name, ids=[nun.node_id for nun in non_unique_nodes]
72
+ )
73
+ for non_unique_node in non_unique_nodes:
74
+ display_label = display_label_map.get(non_unique_node.node_id)
75
+ error_str = f"{display_label or ''}({non_unique_node.node_schema.kind} / {non_unique_node.node_id})"
76
+ error_str += " violates uniqueness constraints for the following attributes: "
77
+ attr_values = [
78
+ f"{attr.attribute_name}={attr.attribute_value}" for attr in non_unique_node.non_unique_attributes
79
+ ]
80
+ error_str += ", ".join(attr_values)
81
+ error_strings.append(error_str)
82
+ if error_strings:
83
+ error_str = "For the following nodes, you must update the uniqueness_constraints on the schema of the node"
84
+ error_str += " to remove the attribute(s) with NULL values or update the data on the nodes to be unique"
85
+ error_str += " now that NULL values are considered during uniqueness validation"
86
+ return MigrationResult(errors=[error_str] + error_strings)
87
+ return MigrationResult()
88
+
89
+
24
90
  class Migration018(InternalSchemaMigration):
25
91
  name: str = "018_validate_nulls_in_uniqueness_constraints"
26
92
  minimum_version: int = 17
27
93
  migrations: Sequence[SchemaMigration] = []
28
94
 
29
95
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
30
- result = MigrationResult()
31
-
32
- return result
96
+ return MigrationResult()
33
97
 
34
98
  async def execute(self, db: InfrahubDatabase) -> MigrationResult:
35
- """
36
- Validate any schema that include optional attributes in the uniqueness constraints
37
-
38
- An update to uniqueness constraint validation now handles NULL values as unique instead of ignoring them
39
- """
40
- default_branch = registry.get_branch_from_registry()
41
- build_component_registry()
42
- component_registry = get_component_registry()
43
- uniqueness_checker = await component_registry.get_component(UniquenessChecker, db=db, branch=default_branch)
44
- non_unique_nodes_by_kind: dict[str, list[NonUniqueNode]] = defaultdict(list)
45
-
46
- manager = SchemaManager()
47
- registry.schema = manager
48
- internal_schema_root = SchemaRoot(**internal_schema)
49
- manager.register_schema(schema=internal_schema_root)
50
- schema_branch = await manager.load_schema_from_db(db=db, branch=default_branch)
51
- manager.set_schema_branch(name=default_branch.name, schema=schema_branch)
52
-
53
- for schema_kind in schema_branch.node_names + schema_branch.generic_names_without_templates:
54
- schema = schema_branch.get(name=schema_kind, duplicate=False)
55
- if not isinstance(schema, NodeSchema | GenericSchema):
56
- continue
57
-
58
- uniqueness_constraint_paths = schema.get_unique_constraint_schema_attribute_paths(
59
- schema_branch=schema_branch
60
- )
61
- includes_optional_attr: bool = False
62
-
63
- for uniqueness_constraint_path in uniqueness_constraint_paths:
64
- for schema_attribute_path in uniqueness_constraint_path.attributes_paths:
65
- if (
66
- schema_attribute_path.attribute_schema
67
- and schema_attribute_path.attribute_schema.optional is True
68
- ):
69
- includes_optional_attr = True
70
- break
71
-
72
- if not includes_optional_attr:
73
- continue
74
-
75
- non_unique_nodes = await uniqueness_checker.check_one_schema(schema=schema)
76
- if non_unique_nodes:
77
- non_unique_nodes_by_kind[schema_kind] = non_unique_nodes
78
-
79
- if not non_unique_nodes_by_kind:
80
- return MigrationResult()
81
-
82
- error_strings = []
83
- for schema_kind, non_unique_nodes in non_unique_nodes_by_kind.items():
84
- display_label_map = await get_display_labels_per_kind(
85
- db=db, kind=schema_kind, branch_name=default_branch.name, ids=[nun.node_id for nun in non_unique_nodes]
86
- )
87
- for non_unique_node in non_unique_nodes:
88
- display_label = display_label_map.get(non_unique_node.node_id)
89
- error_str = f"{display_label or ''}({non_unique_node.node_schema.kind} / {non_unique_node.node_id})"
90
- error_str += " violates uniqueness constraints for the following attributes: "
91
- attr_values = [
92
- f"{attr.attribute_name}={attr.attribute_value}" for attr in non_unique_node.non_unique_attributes
93
- ]
94
- error_str += ", ".join(attr_values)
95
- error_strings.append(error_str)
96
- if error_strings:
97
- error_str = "For the following nodes, you must update the uniqueness_constraints on the schema of the node"
98
- error_str += " to remove the attribute(s) with NULL values or update the data on the nodes to be unique"
99
- error_str += " now that NULL values are considered during uniqueness validation"
100
- return MigrationResult(errors=[error_str] + error_strings)
101
- return MigrationResult()
99
+ return await validate_nulls_in_uniqueness_constraints(db=db)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from infrahub.core.migrations.shared import MigrationResult
6
+ from infrahub.log import get_logger
7
+
8
+ from ..shared import InternalSchemaMigration, SchemaMigration
9
+ from .m018_uniqueness_nulls import validate_nulls_in_uniqueness_constraints
10
+
11
+ if TYPE_CHECKING:
12
+ from infrahub.database import InfrahubDatabase
13
+
14
+ log = get_logger()
15
+
16
+
17
+ class Migration025(InternalSchemaMigration):
18
+ name: str = "025_validate_nulls_in_uniqueness_constraints"
19
+ minimum_version: int = 24
20
+ migrations: Sequence[SchemaMigration] = []
21
+
22
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
23
+ return MigrationResult()
24
+
25
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
26
+ return await validate_nulls_in_uniqueness_constraints(db=db)
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ from typing import TYPE_CHECKING, Sequence
5
+
6
+ from infrahub.core.branch.models import Branch
7
+ from infrahub.core.initialization import initialization
8
+ from infrahub.core.ipam.reconciler import IpamReconciler
9
+ from infrahub.core.manager import NodeManager
10
+ from infrahub.core.migrations.shared import MigrationResult
11
+ from infrahub.core.timestamp import Timestamp
12
+ from infrahub.lock import initialize_lock
13
+ from infrahub.log import get_logger
14
+
15
+ from ..shared import InternalSchemaMigration, SchemaMigration
16
+
17
+ if TYPE_CHECKING:
18
+ from infrahub.database import InfrahubDatabase
19
+
20
+ log = get_logger()
21
+
22
+
23
+ class Migration026(InternalSchemaMigration):
24
+ name: str = "026_prefix_0000_fix"
25
+ minimum_version: int = 25
26
+ migrations: Sequence[SchemaMigration] = []
27
+
28
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
29
+ return MigrationResult()
30
+
31
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
32
+ # load schemas from database into registry
33
+ initialize_lock()
34
+ await initialization(db=db)
35
+
36
+ at = Timestamp()
37
+ for branch in await Branch.get_list(db=db):
38
+ prefix_0000s = await NodeManager.query(
39
+ db=db, schema="BuiltinIPPrefix", branch=branch, filters={"prefix__values": ["0.0.0.0/0", "::/0"]}
40
+ )
41
+ if not prefix_0000s:
42
+ continue
43
+ ipam_reconciler = IpamReconciler(db=db, branch=branch)
44
+ for prefix in prefix_0000s:
45
+ ip_namespace = await prefix.ip_namespace.get_peer(db=db)
46
+ ip_network = ipaddress.ip_network(prefix.prefix.value)
47
+ await ipam_reconciler.reconcile(
48
+ ip_value=ip_network,
49
+ namespace=ip_namespace,
50
+ node_uuid=prefix.get_id(),
51
+ at=at,
52
+ )
53
+
54
+ return MigrationResult()
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from infrahub.core.constants import RelationshipStatus
6
6
  from infrahub.core.graph.schema import GraphAttributeRelationships
7
+ from infrahub.core.schema.generic_schema import GenericSchema
7
8
 
8
9
  from ..query import AttributeMigrationQuery
9
10
  from ..shared import AttributeSchemaMigration
@@ -22,8 +23,20 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
22
23
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
23
24
  self.params.update(branch_params)
24
25
 
26
+ attr_name = self.migration.schema_path.field_name
27
+ kinds_to_ignore = []
28
+ if isinstance(self.migration.new_node_schema, GenericSchema) and attr_name is not None:
29
+ for inheriting_schema_kind in self.migration.new_node_schema.used_by:
30
+ node_schema = db.schema.get_node_schema(
31
+ name=inheriting_schema_kind, branch=self.branch, duplicate=False
32
+ )
33
+ attr_schema = node_schema.get_attribute_or_none(name=attr_name)
34
+ if attr_schema and not attr_schema.inherited:
35
+ kinds_to_ignore.append(inheriting_schema_kind)
36
+
25
37
  self.params["node_kind"] = self.migration.new_schema.kind
26
- self.params["attr_name"] = self.migration.schema_path.field_name
38
+ self.params["kinds_to_ignore"] = kinds_to_ignore
39
+ self.params["attr_name"] = attr_name
27
40
  self.params["current_time"] = self.at.to_string()
28
41
  self.params["branch_name"] = self.branch.name
29
42
  self.params["branch_support"] = self.migration.previous_attribute_schema.get_branch().value
@@ -60,7 +73,8 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
60
73
  query = """
61
74
  // Find all the active nodes
62
75
  MATCH (node:%(node_kind)s)
63
- WHERE exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name }))
76
+ WHERE (size($kinds_to_ignore) = 0 OR NOT any(l IN labels(node) WHERE l IN $kinds_to_ignore))
77
+ AND exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name }))
64
78
  CALL {
65
79
  WITH node
66
80
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
infrahub/core/models.py CHANGED
@@ -529,7 +529,7 @@ class HashableModel(BaseModel):
529
529
 
530
530
  return new_list
531
531
 
532
- def update(self, other: Self) -> Self:
532
+ def update(self, other: HashableModel) -> Self:
533
533
  """Update the current object with the new value from the new one if they are defined.
534
534
 
535
535
  Currently this method works for the following type of fields
@@ -259,7 +259,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
259
259
  )
260
260
  return
261
261
 
262
- if number_pool.node.value == self._schema.kind and number_pool.node_attribute.value == attribute.name:
262
+ if (
263
+ number_pool.node.value in [self._schema.kind] + self._schema.inherit_from
264
+ and number_pool.node_attribute.value == attribute.name
265
+ ):
263
266
  try:
264
267
  next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self)
265
268
  except PoolExhaustedError:
@@ -220,8 +220,13 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
220
220
 
221
221
  violations = []
222
222
  for schema in schemas_to_check:
223
+ schema_filters = list(filters) if filters is not None else []
224
+ for attr_schema in schema.attributes:
225
+ if attr_schema.optional and attr_schema.unique and attr_schema.name not in schema_filters:
226
+ schema_filters.append(attr_schema.name)
227
+
223
228
  schema_violations = await self._get_single_schema_violations(
224
- node=node, node_schema=schema, at=at, filters=filters
229
+ node=node, node_schema=schema, at=at, filters=schema_filters
225
230
  )
226
231
  violations.extend(schema_violations)
227
232
 
@@ -50,7 +50,7 @@ class CoreNumberPool(Node):
50
50
  taken=taken,
51
51
  )
52
52
  if next_number is None:
53
- raise PoolExhaustedError("There are no more addresses available in this pool.")
53
+ raise PoolExhaustedError("There are no more values available in this pool.")
54
54
 
55
55
  return next_number
56
56
 
infrahub/core/registry.py CHANGED
@@ -220,5 +220,23 @@ class Registry:
220
220
  and branch.active_schema_hash.main != default_branch.active_schema_hash.main
221
221
  ]
222
222
 
223
+ async def purge_inactive_branches(
224
+ self, db: InfrahubDatabase, active_branches: list[Branch] | None = None
225
+ ) -> list[str]:
226
+ """Return a list of branches that were purged from the registry."""
227
+ active_branches = active_branches or await self.branch_object.get_list(db=db)
228
+ active_branch_names = [branch.name for branch in active_branches]
229
+ purged_branches: set[str] = set()
230
+
231
+ for branch_name in list(registry.branch.keys()):
232
+ if branch_name not in active_branch_names:
233
+ del registry.branch[branch_name]
234
+ purged_branches.add(branch_name)
235
+
236
+ purged_branches.update(self.schema.purge_inactive_branches(active_branches=active_branch_names))
237
+ purged_branches.update(db.purge_inactive_schemas(active_branches=active_branch_names))
238
+
239
+ return sorted(purged_branches)
240
+
223
241
 
224
242
  registry = Registry()
@@ -11,7 +11,7 @@ from infrahub_sdk.utils import compare_lists, intersection
11
11
  from pydantic import field_validator
12
12
 
13
13
  from infrahub.core.constants import RelationshipCardinality, RelationshipKind
14
- from infrahub.core.models import HashableModelDiff
14
+ from infrahub.core.models import HashableModel, HashableModelDiff
15
15
 
16
16
  from .attribute_schema import AttributeSchema
17
17
  from .generated.base_node_schema import GeneratedBaseNodeSchema
@@ -27,6 +27,16 @@ if TYPE_CHECKING:
27
27
  NODE_METADATA_ATTRIBUTES = ["_source", "_owner"]
28
28
  INHERITED = "INHERITED"
29
29
 
30
+ OPTIONAL_TEXT_FIELDS = [
31
+ "default_filter",
32
+ "description",
33
+ "label",
34
+ "menu_placement",
35
+ "documentation",
36
+ "parent",
37
+ "children",
38
+ ]
39
+
30
40
 
31
41
  class BaseNodeSchema(GeneratedBaseNodeSchema):
32
42
  _exclude_from_hash: list[str] = ["attributes", "relationships"]
@@ -480,6 +490,16 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
480
490
  return UniquenessConstraintType.SUBSET_OF_HFID
481
491
  return UniquenessConstraintType.STANDARD
482
492
 
493
+ def update(self, other: HashableModel) -> Self:
494
+ super().update(other=other)
495
+
496
+ # Allow to specify empty string to remove existing fields values
497
+ for field_name in OPTIONAL_TEXT_FIELDS:
498
+ if getattr(other, field_name, None) == "": # noqa: PLC1901
499
+ setattr(self, field_name, None)
500
+
501
+ return self
502
+
483
503
 
484
504
  @dataclass
485
505
  class SchemaUniquenessConstraintPath:
@@ -18,6 +18,7 @@ from infrahub.core.constants import (
18
18
  DEFAULT_NAME_MIN_LENGTH,
19
19
  DEFAULT_REL_IDENTIFIER_LENGTH,
20
20
  NAME_REGEX,
21
+ NAME_REGEX_OR_EMPTY,
21
22
  NAMESPACE_REGEX,
22
23
  NODE_KIND_REGEX,
23
24
  NODE_NAME_REGEX,
@@ -269,7 +270,7 @@ base_node_schema = SchemaNode(
269
270
  SchemaAttribute(
270
271
  name="default_filter",
271
272
  kind="Text",
272
- regex=str(NAME_REGEX),
273
+ regex=str(NAME_REGEX_OR_EMPTY),
273
274
  description="Default filter used to search for a node in addition to its ID. (deprecated: please use human_friendly_id instead)",
274
275
  optional=True,
275
276
  extra={"update": UpdateSupport.ALLOWED},
@@ -50,7 +50,7 @@ class GeneratedBaseNodeSchema(HashableModel):
50
50
  default_filter: str | None = Field(
51
51
  default=None,
52
52
  description="Default filter used to search for a node in addition to its ID. (deprecated: please use human_friendly_id instead)",
53
- pattern=r"^[a-z0-9\_]+$",
53
+ pattern=r"^[a-z0-9\_]*$",
54
54
  json_schema_extra={"update": "allowed"},
55
55
  )
56
56
  human_friendly_id: list[str] | None = Field(
@@ -155,7 +155,6 @@ class SchemaManager(NodeManager):
155
155
 
156
156
  updated_schema = None
157
157
  if update_db:
158
- schema_diff = None
159
158
  if diff:
160
159
  schema_diff = await self.update_schema_to_db(schema=schema, db=db, branch=branch, diff=diff)
161
160
  else:
@@ -744,3 +743,24 @@ class SchemaManager(NodeManager):
744
743
  """Convert a schema_node object loaded from the database into GenericSchema object."""
745
744
  node_data = await cls._prepare_node_data(schema_node=schema_node, db=db)
746
745
  return GenericSchema(**node_data)
746
+
747
+ def purge_inactive_branches(self, active_branches: list[str]) -> list[str]:
748
+ """Return non active branches that were purged."""
749
+
750
+ hashes_to_keep: set[str] = set()
751
+ for active_branch in active_branches:
752
+ if branch := self._branches.get(active_branch):
753
+ nodes = branch.get_all(include_internal=True, duplicate=False)
754
+ hashes_to_keep.update([node.get_hash() for node in nodes.values()])
755
+
756
+ removed_branches: list[str] = []
757
+ for branch_name in list(self._branches.keys()):
758
+ if branch_name not in active_branches:
759
+ del self._branches[branch_name]
760
+ removed_branches.append(branch_name)
761
+
762
+ for hash_key in list(self._cache.keys()):
763
+ if hash_key not in hashes_to_keep:
764
+ del self._cache[hash_key]
765
+
766
+ return removed_branches
@@ -1005,9 +1005,11 @@ class SchemaBranch:
1005
1005
  generic_schema = self.get_generic(name=name, duplicate=False)
1006
1006
  for attribute in generic_schema.attributes:
1007
1007
  if attribute.computed_attribute and attribute.computed_attribute.kind != ComputedAttributeKind.USER:
1008
- raise ValueError(
1009
- f"{generic_schema.kind}: Attribute {attribute.name!r} computed attributes are only allowed on nodes not generics"
1010
- )
1008
+ for inheriting_node in generic_schema.used_by:
1009
+ node_schema = self.get_node(name=inheriting_node, duplicate=False)
1010
+ self.computed_attributes.validate_generic_inheritance(
1011
+ node=node_schema, attribute=attribute, generic=generic_schema
1012
+ )
1011
1013
 
1012
1014
  def _validate_computed_attribute(self, node: NodeSchema, attribute: AttributeSchema) -> None:
1013
1015
  if not attribute.computed_attribute or attribute.computed_attribute.kind == ComputedAttributeKind.USER:
@@ -1955,10 +1957,9 @@ class SchemaBranch:
1955
1957
  )
1956
1958
  )
1957
1959
 
1958
- if relationship.kind == RelationshipKind.PARENT:
1959
- template_schema.human_friendly_id = [
1960
- f"{relationship.name}__template_name__value"
1961
- ] + template_schema.human_friendly_id
1960
+ parent_hfid = f"{relationship.name}__template_name__value"
1961
+ if relationship.kind == RelationshipKind.PARENT and parent_hfid not in template_schema.human_friendly_id:
1962
+ template_schema.human_friendly_id = [parent_hfid] + template_schema.human_friendly_id
1962
1963
  template_schema.uniqueness_constraints[0].append(relationship.name)
1963
1964
 
1964
1965
  def generate_object_template_from_node(
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field
9
9
  from infrahub.core.schema import AttributeSchema # noqa: TC001
10
10
 
11
11
  if TYPE_CHECKING:
12
- from infrahub.core.schema import NodeSchema, SchemaAttributePath
12
+ from infrahub.core.schema import GenericSchema, NodeSchema, SchemaAttributePath
13
13
 
14
14
 
15
15
  @dataclass
@@ -90,6 +90,7 @@ class ComputedAttributes:
90
90
  ) -> None:
91
91
  self._computed_python_transform_attribute_map: dict[str, list[AttributeSchema]] = transform_attribute_map or {}
92
92
  self._computed_jinja2_attribute_map: dict[str, RegisteredNodeComputedAttribute] = jinja2_attribute_map or {}
93
+ self._defined_from_generic: dict[str, str] = {}
93
94
 
94
95
  def duplicate(self) -> ComputedAttributes:
95
96
  return self.__class__(
@@ -166,6 +167,16 @@ class ComputedAttributes:
166
167
  schema_path.active_relationship_schema.name
167
168
  ].append(deepcopy(source_attribute))
168
169
 
170
+ def validate_generic_inheritance(
171
+ self, node: NodeSchema, attribute: AttributeSchema, generic: GenericSchema
172
+ ) -> None:
173
+ attribute_key = f"{node.kind}__{attribute.name}"
174
+ if duplicate := self._defined_from_generic.get(attribute_key):
175
+ raise ValueError(
176
+ f"{node.kind}: {attribute.name!r} is declared as a computed attribute from multiple generics {sorted([duplicate, generic.kind])}"
177
+ )
178
+ self._defined_from_generic[attribute_key] = generic.kind
179
+
169
180
  def get_impacted_jinja2_targets(self, kind: str, updates: list[str] | None = None) -> list[ComputedAttributeTarget]:
170
181
  if mapping := self._computed_jinja2_attribute_map.get(kind):
171
182
  return mapping.get_targets(updates=updates)
@@ -205,6 +205,16 @@ class InfrahubDatabase:
205
205
  def add_schema(self, schema: SchemaBranch, name: str | None = None) -> None:
206
206
  self._schemas[name or schema.name] = schema
207
207
 
208
+ def purge_inactive_schemas(self, active_branches: list[str]) -> list[str]:
209
+ """Return non active schema branches that were purged."""
210
+ removed_branches: list[str] = []
211
+ for branch_name in list(self._schemas.keys()):
212
+ if branch_name not in active_branches:
213
+ del self._schemas[branch_name]
214
+ removed_branches.append(branch_name)
215
+
216
+ return removed_branches
217
+
208
218
  def start_session(self, read_only: bool = False, schemas: list[SchemaBranch] | None = None) -> InfrahubDatabase:
209
219
  """Create a new InfrahubDatabase object in Session mode."""
210
220
  session_mode = InfrahubDatabaseSessionMode.WRITE
@@ -100,6 +100,9 @@ class BranchMergedEvent(InfrahubEvent):
100
100
 
101
101
  return related
102
102
 
103
+ def get_messages(self) -> list[InfrahubMessage]:
104
+ return [RefreshRegistryBranches()]
105
+
103
106
 
104
107
  class BranchRebasedEvent(InfrahubEvent):
105
108
  """Event generated when a branch has been rebased"""
@@ -2,7 +2,7 @@ from typing import ClassVar
2
2
 
3
3
  from pydantic import Field
4
4
 
5
- from infrahub.core.constants import MutationAction
5
+ from infrahub.core.constants import InfrahubKind, MutationAction
6
6
 
7
7
  from .constants import EVENT_NAMESPACE
8
8
  from .models import EventNode, InfrahubEvent
@@ -21,6 +21,11 @@ class GroupMutatedEvent(InfrahubEvent):
21
21
 
22
22
  def get_related(self) -> list[dict[str, str]]:
23
23
  related = super().get_related()
24
+
25
+ if self.kind in [InfrahubKind.GENERATORGROUP, InfrahubKind.GRAPHQLQUERYGROUP]:
26
+ # Temporary workaround to avoid too large payloads for the related field
27
+ return related
28
+
24
29
  related.append(
25
30
  {
26
31
  "prefect.resource.id": self.node_id,
@@ -7,7 +7,7 @@ from infrahub.core.changelog.models import (
7
7
  RelationshipCardinalityManyChangelog,
8
8
  RelationshipCardinalityOneChangelog,
9
9
  )
10
- from infrahub.core.constants import DiffAction, MutationAction
10
+ from infrahub.core.constants import DiffAction, InfrahubKind, MutationAction
11
11
 
12
12
  from .constants import EVENT_NAMESPACE
13
13
  from .models import InfrahubEvent
@@ -24,6 +24,10 @@ class NodeMutatedEvent(InfrahubEvent):
24
24
 
25
25
  def get_related(self) -> list[dict[str, str]]:
26
26
  related = super().get_related()
27
+ if self.kind in [InfrahubKind.GENERATORGROUP, InfrahubKind.GRAPHQLQUERYGROUP]:
28
+ # Temporary workaround to avoid too large payloads for the related field
29
+ return related
30
+
27
31
  for attribute in self.changelog.attributes.values():
28
32
  related.append(
29
33
  {
@@ -954,7 +954,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
954
954
  source=self.id,
955
955
  is_protected=True,
956
956
  )
957
- obj = await self.sdk.create(kind=CoreCheckDefinition, branch=branch_name, **create_payload)
957
+ obj = await self.sdk.create(kind=CoreCheckDefinition, branch=branch_name, data=create_payload)
958
958
  await obj.save()
959
959
 
960
960
  return obj
@@ -1012,7 +1012,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1012
1012
  source=str(self.id),
1013
1013
  is_protected=True,
1014
1014
  )
1015
- obj = await self.sdk.create(kind=CoreTransformPython, branch=branch_name, **create_payload)
1015
+ obj = await self.sdk.create(kind=CoreTransformPython, branch=branch_name, data=create_payload)
1016
1016
  await obj.save()
1017
1017
  return obj
1018
1018