infrahub-server 1.3.0b5__py3-none-any.whl → 1.3.1__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.
- infrahub/actions/constants.py +36 -79
- infrahub/actions/schema.py +2 -0
- infrahub/cli/db.py +7 -5
- infrahub/cli/upgrade.py +6 -1
- infrahub/core/attribute.py +5 -0
- infrahub/core/constraint/node/runner.py +3 -1
- infrahub/core/convert_object_type/conversion.py +2 -0
- infrahub/core/diff/coordinator.py +8 -1
- infrahub/core/diff/query/delete_query.py +8 -4
- infrahub/core/diff/query/field_specifiers.py +1 -1
- infrahub/core/diff/query/merge.py +2 -2
- infrahub/core/diff/repository/repository.py +4 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
- infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
- infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
- infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
- infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +2 -2
- infrahub/core/migrations/graph/m031_check_number_attributes.py +102 -0
- infrahub/core/migrations/query/attribute_rename.py +1 -1
- infrahub/core/node/__init__.py +70 -37
- infrahub/core/path.py +14 -0
- infrahub/core/query/delete.py +3 -3
- infrahub/core/relationship/constraints/count.py +10 -9
- infrahub/core/relationship/constraints/interface.py +2 -1
- infrahub/core/relationship/constraints/peer_kind.py +2 -1
- infrahub/core/relationship/constraints/peer_parent.py +56 -0
- infrahub/core/relationship/constraints/peer_relatives.py +1 -1
- infrahub/core/relationship/constraints/profiles_kind.py +1 -1
- infrahub/core/schema/attribute_parameters.py +12 -5
- infrahub/core/schema/basenode_schema.py +107 -1
- infrahub/core/schema/definitions/internal.py +8 -1
- infrahub/core/schema/generated/relationship_schema.py +6 -1
- infrahub/core/schema/schema_branch.py +53 -13
- infrahub/core/validators/__init__.py +2 -1
- infrahub/core/validators/attribute/min_max.py +7 -2
- infrahub/core/validators/relationship/peer.py +174 -4
- infrahub/database/__init__.py +0 -1
- infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
- infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
- infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
- infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
- infrahub/dependencies/registry.py +2 -0
- infrahub/git/tasks.py +1 -0
- infrahub/graphql/app.py +5 -1
- infrahub/graphql/mutations/convert_object_type.py +16 -7
- infrahub/graphql/mutations/relationship.py +32 -0
- infrahub/graphql/queries/convert_object_type_mapping.py +3 -5
- infrahub/message_bus/operations/refresh/registry.py +3 -6
- infrahub/pools/models.py +14 -0
- infrahub/pools/tasks.py +71 -1
- infrahub/services/adapters/message_bus/nats.py +5 -1
- infrahub/services/scheduler.py +5 -1
- infrahub_sdk/ctl/generator.py +4 -4
- infrahub_sdk/ctl/repository.py +1 -1
- infrahub_sdk/node/__init__.py +2 -0
- infrahub_sdk/node/node.py +166 -93
- infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
- infrahub_sdk/query_groups.py +4 -3
- infrahub_sdk/utils.py +7 -20
- infrahub_sdk/yaml.py +6 -5
- {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/METADATA +2 -2
- {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/RECORD +68 -63
- {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/entry_points.txt +0 -0
infrahub/core/node/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ from infrahub_sdk.template import Jinja2Template
|
|
|
7
7
|
from infrahub_sdk.utils import is_valid_uuid
|
|
8
8
|
from infrahub_sdk.uuidt import UUIDT
|
|
9
9
|
|
|
10
|
+
from infrahub import lock
|
|
10
11
|
from infrahub.core import registry
|
|
11
12
|
from infrahub.core.changelog.models import NodeChangelog
|
|
12
13
|
from infrahub.core.constants import (
|
|
@@ -34,6 +35,7 @@ from infrahub.core.schema import (
|
|
|
34
35
|
from infrahub.core.schema.attribute_parameters import NumberPoolParameters
|
|
35
36
|
from infrahub.core.timestamp import Timestamp
|
|
36
37
|
from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
|
|
38
|
+
from infrahub.pools.models import NumberPoolLockDefinition
|
|
37
39
|
from infrahub.types import ATTRIBUTE_TYPES
|
|
38
40
|
|
|
39
41
|
from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
|
|
@@ -68,7 +70,9 @@ log = get_logger()
|
|
|
68
70
|
|
|
69
71
|
class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
70
72
|
@classmethod
|
|
71
|
-
def __init_subclass_with_meta__(
|
|
73
|
+
def __init_subclass_with_meta__(
|
|
74
|
+
cls, _meta: BaseNodeOptions | None = None, default_filter: None = None, **options: dict[str, Any]
|
|
75
|
+
) -> None:
|
|
72
76
|
if not _meta:
|
|
73
77
|
_meta = BaseNodeOptions(cls)
|
|
74
78
|
|
|
@@ -271,7 +275,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
271
275
|
)
|
|
272
276
|
except NodeNotFoundError:
|
|
273
277
|
if number_pool_parameters:
|
|
274
|
-
number_pool = await self.
|
|
278
|
+
number_pool = await self._fetch_or_create_number_pool(
|
|
275
279
|
db=db, attribute=attribute, number_pool_parameters=number_pool_parameters
|
|
276
280
|
)
|
|
277
281
|
|
|
@@ -306,35 +310,49 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
306
310
|
)
|
|
307
311
|
)
|
|
308
312
|
|
|
309
|
-
async def
|
|
313
|
+
async def _fetch_or_create_number_pool(
|
|
310
314
|
self, db: InfrahubDatabase, attribute: BaseAttribute, number_pool_parameters: NumberPoolParameters
|
|
311
315
|
) -> CoreNumberPool:
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
316
|
+
number_pool_from_db: CoreNumberPool | None = None
|
|
317
|
+
lock_definition = NumberPoolLockDefinition(pool_id=str(number_pool_parameters.number_pool_id))
|
|
318
|
+
async with lock.registry.get(
|
|
319
|
+
name=lock_definition.lock_name, namespace=lock_definition.namespace_name, local=False
|
|
320
|
+
):
|
|
321
|
+
try:
|
|
322
|
+
number_pool_from_db = await registry.manager.get_one_by_id_or_default_filter(
|
|
323
|
+
db=db, id=str(number_pool_parameters.number_pool_id), kind=CoreNumberPool
|
|
324
|
+
)
|
|
325
|
+
except NodeNotFoundError:
|
|
326
|
+
schema = db.schema.get_node_schema(name="CoreNumberPool", duplicate=False)
|
|
327
|
+
|
|
328
|
+
pool_node = self._schema.kind
|
|
329
|
+
schema_attribute = self._schema.get_attribute(attribute.schema.name)
|
|
330
|
+
if schema_attribute.inherited:
|
|
331
|
+
for generic_name in self._schema.inherit_from:
|
|
332
|
+
generic_node = db.schema.get_generic_schema(name=generic_name, duplicate=False)
|
|
333
|
+
if attribute.schema.name in generic_node.attribute_names:
|
|
334
|
+
pool_node = generic_node.kind
|
|
335
|
+
break
|
|
336
|
+
|
|
337
|
+
number_pool = await Node.init(db=db, schema=schema, branch=self._branch)
|
|
338
|
+
await number_pool.new(
|
|
339
|
+
db=db,
|
|
340
|
+
id=number_pool_parameters.number_pool_id,
|
|
341
|
+
name=f"{pool_node}.{attribute.schema.name} [{number_pool_parameters.number_pool_id}]",
|
|
342
|
+
node=pool_node,
|
|
343
|
+
node_attribute=attribute.schema.name,
|
|
344
|
+
start_range=number_pool_parameters.start_range,
|
|
345
|
+
end_range=number_pool_parameters.end_range,
|
|
346
|
+
pool_type=NumberPoolType.SCHEMA.value,
|
|
347
|
+
)
|
|
348
|
+
await number_pool.save(db=db)
|
|
349
|
+
|
|
335
350
|
# Do a lookup of the number pool to get the correct mapped type from the registry
|
|
336
351
|
# without this we don't get access to the .get_resource() method.
|
|
337
|
-
|
|
352
|
+
created_pool: CoreNumberPool = number_pool_from_db or await registry.manager.get_one_by_id_or_default_filter(
|
|
353
|
+
db=db, id=number_pool.id, kind=CoreNumberPool
|
|
354
|
+
)
|
|
355
|
+
return created_pool
|
|
338
356
|
|
|
339
357
|
async def handle_object_template(self, fields: dict, db: InfrahubDatabase, errors: list) -> None:
|
|
340
358
|
"""Fill the `fields` parameters with values from an object template if one is in use."""
|
|
@@ -541,17 +559,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
541
559
|
relationship_attribute: RelationshipManager = getattr(
|
|
542
560
|
self, attribute_path.active_relationship_schema.name
|
|
543
561
|
)
|
|
544
|
-
peer
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
562
|
+
if peer := await relationship_attribute.get_peer(db=db, raise_on_error=False):
|
|
563
|
+
related_node = await registry.manager.get_one_by_id_or_default_filter(
|
|
564
|
+
db=db,
|
|
565
|
+
id=peer.id,
|
|
566
|
+
kind=attribute_path.active_relationship_schema.peer,
|
|
567
|
+
branch=self._branch.name,
|
|
568
|
+
)
|
|
549
569
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
570
|
+
attribute: BaseAttribute = getattr(
|
|
571
|
+
getattr(related_node, attribute_path.active_attribute_schema.name),
|
|
572
|
+
attribute_path.active_attribute_property_name,
|
|
573
|
+
)
|
|
574
|
+
variables[variable] = attribute
|
|
575
|
+
else:
|
|
576
|
+
variables[variable] = None
|
|
555
577
|
|
|
556
578
|
elif attribute_path.is_type_attribute:
|
|
557
579
|
attribute = getattr(
|
|
@@ -939,6 +961,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
939
961
|
if relationship.kind == RelationshipKind.PARENT:
|
|
940
962
|
return relationship.name
|
|
941
963
|
|
|
964
|
+
return None
|
|
965
|
+
|
|
942
966
|
async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
|
|
943
967
|
object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
|
|
944
968
|
return (
|
|
@@ -962,3 +986,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
962
986
|
for name in self._relationships:
|
|
963
987
|
relm: RelationshipManager = getattr(self, name)
|
|
964
988
|
relm.validate()
|
|
989
|
+
|
|
990
|
+
async def get_parent_relationship_peer(self, db: InfrahubDatabase, name: str) -> Node | None:
|
|
991
|
+
"""When a node has a parent relationship of a given name, this method returns the peer of that relationship."""
|
|
992
|
+
relationship = self.get_schema().get_relationship(name=name)
|
|
993
|
+
if relationship.kind != RelationshipKind.PARENT:
|
|
994
|
+
raise ValueError(f"Relationship '{name}' is not of kind 'parent'")
|
|
995
|
+
|
|
996
|
+
relm: RelationshipManager = getattr(self, name)
|
|
997
|
+
return await relm.get_peer(db=db)
|
infrahub/core/path.py
CHANGED
|
@@ -63,6 +63,20 @@ class DataPath(InfrahubPath):
|
|
|
63
63
|
peer_id: str | None = Field(default=None, description="")
|
|
64
64
|
value: Any | None = Field(default=None, description="Optional value of the resource")
|
|
65
65
|
|
|
66
|
+
def __hash__(self) -> int:
|
|
67
|
+
return hash(
|
|
68
|
+
(
|
|
69
|
+
self.branch,
|
|
70
|
+
self.path_type,
|
|
71
|
+
self.node_id,
|
|
72
|
+
self.kind,
|
|
73
|
+
self.field_name,
|
|
74
|
+
self.property_name,
|
|
75
|
+
self.peer_id,
|
|
76
|
+
str(self.value),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
66
80
|
@property
|
|
67
81
|
def resource_type(self) -> PathResourceType:
|
|
68
82
|
return PathResourceType.DATA
|
infrahub/core/query/delete.py
CHANGED
|
@@ -21,7 +21,7 @@ class DeleteAfterTimeQuery(Query):
|
|
|
21
21
|
// ---------------------
|
|
22
22
|
// Reset edges with to time after timestamp
|
|
23
23
|
// ---------------------
|
|
24
|
-
CALL {
|
|
24
|
+
CALL () {
|
|
25
25
|
OPTIONAL MATCH (p)-[r]-(q)
|
|
26
26
|
WHERE r.to > $timestamp
|
|
27
27
|
SET r.to = NULL
|
|
@@ -33,7 +33,7 @@ class DeleteAfterTimeQuery(Query):
|
|
|
33
33
|
// ---------------------
|
|
34
34
|
// Delete edges with from time after timestamp timestamp
|
|
35
35
|
// ---------------------
|
|
36
|
-
CALL {
|
|
36
|
+
CALL () {
|
|
37
37
|
OPTIONAL MATCH (p)-[r]->(q)
|
|
38
38
|
WHERE r.from > $timestamp
|
|
39
39
|
DELETE r
|
|
@@ -49,7 +49,7 @@ class DeleteAfterTimeQuery(Query):
|
|
|
49
49
|
// ---------------------
|
|
50
50
|
// Delete edges with from time after timestamp timestamp
|
|
51
51
|
// ---------------------
|
|
52
|
-
CALL {
|
|
52
|
+
CALL () {
|
|
53
53
|
OPTIONAL MATCH (p)-[r]->(q)
|
|
54
54
|
WHERE r.from > $timestamp
|
|
55
55
|
DELETE r
|
|
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
from infrahub.core import registry
|
|
4
4
|
from infrahub.core.branch import Branch
|
|
5
5
|
from infrahub.core.constants import RelationshipCardinality, RelationshipDirection
|
|
6
|
+
from infrahub.core.node import Node
|
|
6
7
|
from infrahub.core.query.relationship import RelationshipCountPerNodeQuery
|
|
7
8
|
from infrahub.core.schema import MainSchemaTypes
|
|
8
9
|
from infrahub.database import InfrahubDatabase
|
|
@@ -25,7 +26,7 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
|
|
|
25
26
|
self.db = db
|
|
26
27
|
self.branch = branch
|
|
27
28
|
|
|
28
|
-
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None: # noqa: ARG002
|
|
29
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
|
|
29
30
|
branch = await registry.get_branch(db=self.db) if not self.branch else self.branch
|
|
30
31
|
|
|
31
32
|
# NOTE adding resolve here because we need to retrieve the real ID
|
|
@@ -63,7 +64,7 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
|
|
|
63
64
|
|
|
64
65
|
query = await RelationshipCountPerNodeQuery.init(
|
|
65
66
|
db=self.db,
|
|
66
|
-
node_ids=[
|
|
67
|
+
node_ids=[n.uuid for n in nodes_to_validate],
|
|
67
68
|
identifier=relm.schema.identifier,
|
|
68
69
|
direction=relm.schema.direction.neighbor_direction,
|
|
69
70
|
branch=branch,
|
|
@@ -74,14 +75,14 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
|
|
|
74
75
|
# Need to adjust the number based on what we will add / remove
|
|
75
76
|
# +1 for max_count
|
|
76
77
|
# -1 for min_count
|
|
77
|
-
for
|
|
78
|
-
if
|
|
78
|
+
for node_to_validate in nodes_to_validate:
|
|
79
|
+
if node_to_validate.max_count and count_per_peer[node_to_validate.uuid] + 1 > node_to_validate.max_count:
|
|
79
80
|
raise ValidationError(
|
|
80
|
-
f"Node {
|
|
81
|
-
f"for {relm.schema.identifier}, maximum of {
|
|
81
|
+
f"Node {node_to_validate.uuid} has {count_per_peer[node_to_validate.uuid] + 1} peers "
|
|
82
|
+
f"for {relm.schema.identifier}, maximum of {node_to_validate.max_count} allowed",
|
|
82
83
|
)
|
|
83
|
-
if
|
|
84
|
+
if node_to_validate.min_count and count_per_peer[node_to_validate.uuid] - 1 < node_to_validate.min_count:
|
|
84
85
|
raise ValidationError(
|
|
85
|
-
f"Node {
|
|
86
|
-
f"for {relm.schema.identifier}, no fewer than {
|
|
86
|
+
f"Node {node_to_validate.uuid} has {count_per_peer[node_to_validate.uuid] - 1} peers "
|
|
87
|
+
f"for {relm.schema.identifier}, no fewer than {node_to_validate.min_count} allowed",
|
|
87
88
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
|
|
3
|
+
from infrahub.core.node import Node
|
|
3
4
|
from infrahub.core.schema import MainSchemaTypes
|
|
4
5
|
|
|
5
6
|
from ..model import RelationshipManager
|
|
@@ -7,4 +8,4 @@ from ..model import RelationshipManager
|
|
|
7
8
|
|
|
8
9
|
class RelationshipManagerConstraintInterface(ABC):
|
|
9
10
|
@abstractmethod
|
|
10
|
-
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None: ...
|
|
11
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: ...
|
|
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
from infrahub.core import registry
|
|
4
4
|
from infrahub.core.branch import Branch
|
|
5
5
|
from infrahub.core.constants import RelationshipCardinality
|
|
6
|
+
from infrahub.core.node import Node
|
|
6
7
|
from infrahub.core.query.node import NodeListGetInfoQuery
|
|
7
8
|
from infrahub.core.schema import MainSchemaTypes
|
|
8
9
|
from infrahub.core.schema.generic_schema import GenericSchema
|
|
@@ -26,7 +27,7 @@ class RelationshipPeerKindConstraint(RelationshipManagerConstraintInterface):
|
|
|
26
27
|
self.db = db
|
|
27
28
|
self.branch = branch
|
|
28
29
|
|
|
29
|
-
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None: # noqa: ARG002
|
|
30
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
|
|
30
31
|
branch = await registry.get_branch(db=self.db) if not self.branch else self.branch
|
|
31
32
|
peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch, duplicate=False)
|
|
32
33
|
if isinstance(peer_schema, GenericSchema):
|
|
@@ -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
|
+
)
|
|
@@ -58,7 +58,7 @@ class RelationshipPeerRelativesConstraint(RelationshipManagerConstraintInterface
|
|
|
58
58
|
f"for their '{node.schema.kind}.{relationship_name}' relationship"
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None:
|
|
61
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
|
|
62
62
|
if relm.schema.cardinality != RelationshipCardinality.MANY or not relm.schema.common_relatives:
|
|
63
63
|
return
|
|
64
64
|
|
|
@@ -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
|
|
|
@@ -8,6 +8,7 @@ from pydantic import ConfigDict, Field, model_validator
|
|
|
8
8
|
from infrahub import config
|
|
9
9
|
from infrahub.core.constants.schema import UpdateSupport
|
|
10
10
|
from infrahub.core.models import HashableModel
|
|
11
|
+
from infrahub.exceptions import ValidationError
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParameters]:
|
|
@@ -124,16 +125,22 @@ class NumberAttributeParameters(AttributeParameters):
|
|
|
124
125
|
return ranges
|
|
125
126
|
|
|
126
127
|
def is_valid_value(self, value: int) -> bool:
|
|
127
|
-
|
|
128
|
+
try:
|
|
129
|
+
self.check_valid_value(value=value, name="UNUSED")
|
|
130
|
+
except ValidationError:
|
|
128
131
|
return False
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
def check_valid_value(self, value: int, name: str) -> None:
|
|
135
|
+
if self.min_value is not None and value < self.min_value:
|
|
136
|
+
raise ValidationError({name: f"{value} is lower than the minimum allowed value {self.min_value!r}"})
|
|
129
137
|
if self.max_value is not None and value > self.max_value:
|
|
130
|
-
|
|
138
|
+
raise ValidationError({name: f"{value} is higher than the maximum allowed value {self.max_value!r}"})
|
|
131
139
|
if value in self.get_excluded_single_values():
|
|
132
|
-
|
|
140
|
+
raise ValidationError({name: f"{value} is in the excluded values"})
|
|
133
141
|
for start, end in self.get_excluded_ranges():
|
|
134
142
|
if start <= value <= end:
|
|
135
|
-
|
|
136
|
-
return True
|
|
143
|
+
raise ValidationError({name: f"{value} is in an the excluded range {start}-{end}"})
|
|
137
144
|
|
|
138
145
|
|
|
139
146
|
class NumberPoolParameters(AttributeParameters):
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import keyword
|
|
5
5
|
import os
|
|
6
|
+
from collections import defaultdict
|
|
6
7
|
from dataclasses import asdict, dataclass
|
|
7
8
|
from enum import Enum
|
|
8
9
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, overload
|
|
@@ -10,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, overload
|
|
|
10
11
|
from infrahub_sdk.utils import compare_lists, intersection
|
|
11
12
|
from pydantic import field_validator
|
|
12
13
|
|
|
13
|
-
from infrahub.core.constants import RelationshipCardinality, RelationshipKind
|
|
14
|
+
from infrahub.core.constants import HashableModelState, RelationshipCardinality, RelationshipKind
|
|
14
15
|
from infrahub.core.models import HashableModel, HashableModelDiff
|
|
15
16
|
|
|
16
17
|
from .attribute_schema import AttributeSchema, get_attribute_schema_class_for_kind
|
|
@@ -514,7 +515,86 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
514
515
|
return UniquenessConstraintType.SUBSET_OF_HFID
|
|
515
516
|
return UniquenessConstraintType.STANDARD
|
|
516
517
|
|
|
518
|
+
def _update_schema_paths(
|
|
519
|
+
self, schema_paths_list: list[str], field_name_update_map: dict[str, str], deleted_field_names: set[str]
|
|
520
|
+
) -> list[str]:
|
|
521
|
+
"""
|
|
522
|
+
For each schema_path (eg name__value, device__name_value), update the field name if the current name is
|
|
523
|
+
in field_name_update_map, remove the path if the field name is in deleted_field_names
|
|
524
|
+
"""
|
|
525
|
+
updated_element_list = []
|
|
526
|
+
for schema_path in schema_paths_list:
|
|
527
|
+
split_path = schema_path.split("__", maxsplit=1)
|
|
528
|
+
current_field_name = split_path[0]
|
|
529
|
+
if current_field_name in deleted_field_names:
|
|
530
|
+
continue
|
|
531
|
+
new_field_name = field_name_update_map.get(current_field_name)
|
|
532
|
+
if not new_field_name:
|
|
533
|
+
updated_element_list.append(schema_path)
|
|
534
|
+
continue
|
|
535
|
+
rest_of_path = f"__{split_path[1]}" if len(split_path) > 1 else ""
|
|
536
|
+
new_element_str = f"{new_field_name}{rest_of_path}"
|
|
537
|
+
updated_element_list.append(new_element_str)
|
|
538
|
+
return updated_element_list
|
|
539
|
+
|
|
540
|
+
def handle_field_renames_and_deletes(self, other: BaseNodeSchema) -> None:
|
|
541
|
+
properties_to_update = [self.uniqueness_constraints, self.human_friendly_id, self.display_labels, self.order_by]
|
|
542
|
+
if not any(p for p in properties_to_update):
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
deleted_names: set[str] = set()
|
|
546
|
+
field_names_by_id = defaultdict(list)
|
|
547
|
+
for field in self.attributes + self.relationships:
|
|
548
|
+
if not field.id:
|
|
549
|
+
continue
|
|
550
|
+
field_names_by_id[field.id].append(field.name)
|
|
551
|
+
for field in other.attributes + other.relationships:
|
|
552
|
+
# identify fields deleted in the other schema
|
|
553
|
+
if field.state is HashableModelState.ABSENT:
|
|
554
|
+
deleted_names.add(field.name)
|
|
555
|
+
if not field.id:
|
|
556
|
+
continue
|
|
557
|
+
if field.name not in field_names_by_id[field.id]:
|
|
558
|
+
field_names_by_id[field.id].append(field.name)
|
|
559
|
+
# identify fields renamed from this schema to the other schema
|
|
560
|
+
renamed_field_name_map = {v[0]: v[-1] for v in field_names_by_id.values() if len(v) > 1}
|
|
561
|
+
|
|
562
|
+
if self.uniqueness_constraints:
|
|
563
|
+
updated_constraints = []
|
|
564
|
+
for constraint in self.uniqueness_constraints:
|
|
565
|
+
updated_constraint = self._update_schema_paths(
|
|
566
|
+
schema_paths_list=constraint,
|
|
567
|
+
field_name_update_map=renamed_field_name_map,
|
|
568
|
+
deleted_field_names=deleted_names,
|
|
569
|
+
)
|
|
570
|
+
if updated_constraint:
|
|
571
|
+
updated_constraints.append(updated_constraint)
|
|
572
|
+
self.uniqueness_constraints = updated_constraints
|
|
573
|
+
if self.human_friendly_id:
|
|
574
|
+
self.human_friendly_id = self._update_schema_paths(
|
|
575
|
+
schema_paths_list=self.human_friendly_id,
|
|
576
|
+
field_name_update_map=renamed_field_name_map,
|
|
577
|
+
deleted_field_names=deleted_names,
|
|
578
|
+
)
|
|
579
|
+
if self.display_labels:
|
|
580
|
+
self.display_labels = self._update_schema_paths(
|
|
581
|
+
schema_paths_list=self.display_labels,
|
|
582
|
+
field_name_update_map=renamed_field_name_map,
|
|
583
|
+
deleted_field_names=deleted_names,
|
|
584
|
+
)
|
|
585
|
+
if self.order_by:
|
|
586
|
+
self.order_by = self._update_schema_paths(
|
|
587
|
+
schema_paths_list=self.order_by,
|
|
588
|
+
field_name_update_map=renamed_field_name_map,
|
|
589
|
+
deleted_field_names=deleted_names,
|
|
590
|
+
)
|
|
591
|
+
|
|
517
592
|
def update(self, other: HashableModel) -> Self:
|
|
593
|
+
# handle renamed/deleted field updates for schema properties here
|
|
594
|
+
# so that they can still be overridden during the call to `update()` below
|
|
595
|
+
if isinstance(other, BaseNodeSchema):
|
|
596
|
+
self.handle_field_renames_and_deletes(other=other)
|
|
597
|
+
|
|
518
598
|
super().update(other=other)
|
|
519
599
|
|
|
520
600
|
# Allow to specify empty string to remove existing fields values
|
|
@@ -551,6 +631,24 @@ class SchemaAttributePath:
|
|
|
551
631
|
attribute_schema: AttributeSchema | None = None
|
|
552
632
|
attribute_property_name: str | None = None
|
|
553
633
|
|
|
634
|
+
def __str__(self) -> str:
|
|
635
|
+
return self.to_string()
|
|
636
|
+
|
|
637
|
+
def to_string(self, field_name_override: str | None = None) -> str:
|
|
638
|
+
str_path = ""
|
|
639
|
+
if self.relationship_schema:
|
|
640
|
+
str_path += field_name_override or self.relationship_schema.name
|
|
641
|
+
if self.attribute_schema:
|
|
642
|
+
if str_path:
|
|
643
|
+
str_path += "__"
|
|
644
|
+
attr_name = self.attribute_schema.name
|
|
645
|
+
else:
|
|
646
|
+
attr_name = field_name_override or self.attribute_schema.name
|
|
647
|
+
str_path += attr_name
|
|
648
|
+
if self.attribute_property_name:
|
|
649
|
+
str_path += f"__{self.attribute_property_name}"
|
|
650
|
+
return str_path
|
|
651
|
+
|
|
554
652
|
@property
|
|
555
653
|
def is_type_attribute(self) -> bool:
|
|
556
654
|
return bool(self.attribute_schema and not self.related_schema and not self.relationship_schema)
|
|
@@ -563,6 +661,14 @@ class SchemaAttributePath:
|
|
|
563
661
|
def has_property(self) -> bool:
|
|
564
662
|
return bool(self.attribute_property_name)
|
|
565
663
|
|
|
664
|
+
@property
|
|
665
|
+
def field_name(self) -> str | None:
|
|
666
|
+
if self.relationship_schema:
|
|
667
|
+
return self.relationship_schema.name
|
|
668
|
+
if self.attribute_schema:
|
|
669
|
+
return self.attribute_schema.name
|
|
670
|
+
return None
|
|
671
|
+
|
|
566
672
|
@property
|
|
567
673
|
def active_relationship_schema(self) -> RelationshipSchema:
|
|
568
674
|
if self.relationship_schema:
|
|
@@ -754,13 +754,20 @@ relationship_schema = SchemaNode(
|
|
|
754
754
|
optional=True,
|
|
755
755
|
extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
|
|
756
756
|
),
|
|
757
|
+
SchemaAttribute(
|
|
758
|
+
name="common_parent",
|
|
759
|
+
kind="Text",
|
|
760
|
+
optional=True,
|
|
761
|
+
description="Name of a parent relationship on the peer schema that must share the same related object with the object's parent.",
|
|
762
|
+
extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
|
|
763
|
+
),
|
|
757
764
|
SchemaAttribute(
|
|
758
765
|
name="common_relatives",
|
|
759
766
|
kind="List",
|
|
760
767
|
internal_kind=str,
|
|
761
768
|
optional=True,
|
|
762
769
|
description="List of relationship names on the peer schema for which all objects must share the same set of peers.",
|
|
763
|
-
extra={"update": UpdateSupport.
|
|
770
|
+
extra={"update": UpdateSupport.ALLOWED},
|
|
764
771
|
),
|
|
765
772
|
SchemaAttribute(
|
|
766
773
|
name="order_weight",
|
|
@@ -73,10 +73,15 @@ class GeneratedRelationshipSchema(HashableModel):
|
|
|
73
73
|
description="Defines the maximum objects allowed on the other side of the relationship.",
|
|
74
74
|
json_schema_extra={"update": "validate_constraint"},
|
|
75
75
|
)
|
|
76
|
+
common_parent: str | None = Field(
|
|
77
|
+
default=None,
|
|
78
|
+
description="Name of a parent relationship on the peer schema that must share the same related object with the object's parent.",
|
|
79
|
+
json_schema_extra={"update": "validate_constraint"},
|
|
80
|
+
)
|
|
76
81
|
common_relatives: list[str] | None = Field(
|
|
77
82
|
default=None,
|
|
78
83
|
description="List of relationship names on the peer schema for which all objects must share the same set of peers.",
|
|
79
|
-
json_schema_extra={"update": "
|
|
84
|
+
json_schema_extra={"update": "allowed"},
|
|
80
85
|
)
|
|
81
86
|
order_weight: int | None = Field(
|
|
82
87
|
default=None,
|