infrahub-server 1.5.0b0__py3-none-any.whl → 1.5.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.
- infrahub/actions/tasks.py +8 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/cli/db.py +24 -0
- infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
- infrahub/core/attribute.py +3 -3
- infrahub/core/branch/tasks.py +2 -1
- infrahub/core/changelog/models.py +4 -12
- infrahub/core/constants/infrahubkind.py +1 -0
- infrahub/core/diff/model/path.py +4 -0
- infrahub/core/diff/payload_builder.py +1 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/ipam/utilization.py +1 -1
- infrahub/core/manager.py +6 -3
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
- infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
- infrahub/core/migrations/schema/node_attribute_add.py +5 -2
- infrahub/core/migrations/shared.py +5 -6
- infrahub/core/node/__init__.py +142 -40
- infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/standard.py +1 -1
- infrahub/core/protocols.py +7 -1
- infrahub/core/query/node.py +14 -1
- infrahub/core/registry.py +2 -2
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/model.py +1 -1
- infrahub/core/schema/basenode_schema.py +42 -2
- infrahub/core/schema/definitions/core/__init__.py +2 -0
- infrahub/core/schema/definitions/core/generator.py +2 -0
- infrahub/core/schema/definitions/core/group.py +16 -2
- infrahub/core/schema/definitions/internal.py +14 -1
- infrahub/core/schema/generated/base_node_schema.py +6 -1
- infrahub/core/schema/node_schema.py +5 -2
- infrahub/core/schema/schema_branch.py +134 -0
- infrahub/core/schema/schema_branch_display.py +123 -0
- infrahub/core/schema/schema_branch_hfid.py +114 -0
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/determiner.py +12 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/core/validators/tasks.py +1 -1
- infrahub/display_labels/__init__.py +0 -0
- infrahub/display_labels/gather.py +48 -0
- infrahub/display_labels/models.py +240 -0
- infrahub/display_labels/tasks.py +186 -0
- infrahub/display_labels/triggers.py +22 -0
- infrahub/events/group_action.py +1 -1
- infrahub/events/node_action.py +1 -1
- infrahub/generators/constants.py +7 -0
- infrahub/generators/models.py +7 -0
- infrahub/generators/tasks.py +31 -15
- infrahub/git/integrator.py +22 -14
- infrahub/graphql/analyzer.py +1 -1
- infrahub/graphql/mutations/display_label.py +111 -0
- infrahub/graphql/mutations/generator.py +25 -7
- infrahub/graphql/mutations/hfid.py +118 -0
- infrahub/graphql/mutations/relationship.py +2 -2
- infrahub/graphql/mutations/resource_manager.py +2 -2
- infrahub/graphql/mutations/schema.py +5 -5
- infrahub/graphql/queries/resource_manager.py +1 -1
- infrahub/graphql/resolvers/resolver.py +2 -0
- infrahub/graphql/schema.py +4 -0
- infrahub/groups/tasks.py +1 -1
- infrahub/hfid/__init__.py +0 -0
- infrahub/hfid/gather.py +48 -0
- infrahub/hfid/models.py +240 -0
- infrahub/hfid/tasks.py +185 -0
- infrahub/hfid/triggers.py +22 -0
- infrahub/lock.py +15 -4
- infrahub/middleware.py +26 -1
- infrahub/proposed_change/tasks.py +10 -1
- infrahub/server.py +16 -3
- infrahub/services/__init__.py +8 -5
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/tasks.py +3 -0
- infrahub/workflows/catalogue.py +72 -0
- infrahub/workflows/initialization.py +16 -0
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/ctl/cli_commands.py +2 -0
- infrahub_sdk/ctl/generator.py +4 -0
- infrahub_sdk/ctl/graphql.py +184 -0
- infrahub_sdk/ctl/schema.py +6 -2
- infrahub_sdk/generator.py +7 -1
- infrahub_sdk/graphql/__init__.py +12 -0
- infrahub_sdk/graphql/constants.py +1 -0
- infrahub_sdk/graphql/plugin.py +85 -0
- infrahub_sdk/graphql/query.py +77 -0
- infrahub_sdk/{graphql.py → graphql/renderers.py} +81 -73
- infrahub_sdk/graphql/utils.py +40 -0
- infrahub_sdk/protocols.py +14 -0
- infrahub_sdk/schema/__init__.py +38 -0
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/object.py +84 -10
- infrahub_sdk/spec/range_expansion.py +1 -1
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +5 -4
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +104 -79
- infrahub_testcontainers/container.py +1 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -10,8 +10,7 @@ from infrahub.core.path import SchemaPath # noqa: TC001
|
|
|
10
10
|
from infrahub.core.query import Query # noqa: TC001
|
|
11
11
|
from infrahub.core.schema import (
|
|
12
12
|
AttributeSchema,
|
|
13
|
-
|
|
14
|
-
NodeSchema,
|
|
13
|
+
MainSchemaTypes,
|
|
15
14
|
RelationshipSchema,
|
|
16
15
|
SchemaRoot,
|
|
17
16
|
internal_schema,
|
|
@@ -43,8 +42,8 @@ class SchemaMigration(BaseModel):
|
|
|
43
42
|
name: str = Field(..., description="Name of the migration")
|
|
44
43
|
queries: Sequence[type[MigrationQuery]] = Field(..., description="List of queries to execute for this migration")
|
|
45
44
|
|
|
46
|
-
new_node_schema:
|
|
47
|
-
previous_node_schema:
|
|
45
|
+
new_node_schema: MainSchemaTypes | None = None
|
|
46
|
+
previous_node_schema: MainSchemaTypes | None = None
|
|
48
47
|
schema_path: SchemaPath
|
|
49
48
|
|
|
50
49
|
async def execute_pre_queries(
|
|
@@ -91,13 +90,13 @@ class SchemaMigration(BaseModel):
|
|
|
91
90
|
return result
|
|
92
91
|
|
|
93
92
|
@property
|
|
94
|
-
def new_schema(self) ->
|
|
93
|
+
def new_schema(self) -> MainSchemaTypes:
|
|
95
94
|
if self.new_node_schema:
|
|
96
95
|
return self.new_node_schema
|
|
97
96
|
raise ValueError("new_node_schema hasn't been initialized")
|
|
98
97
|
|
|
99
98
|
@property
|
|
100
|
-
def previous_schema(self) ->
|
|
99
|
+
def previous_schema(self) -> MainSchemaTypes:
|
|
101
100
|
if self.previous_node_schema:
|
|
102
101
|
return self.previous_node_schema
|
|
103
102
|
raise ValueError("previous_node_schema hasn't been initialized")
|
infrahub/core/node/__init__.py
CHANGED
|
@@ -47,6 +47,7 @@ from ..query.relationship import RelationshipDeleteAllQuery
|
|
|
47
47
|
from ..relationship import RelationshipManager
|
|
48
48
|
from ..utils import update_relationships_to
|
|
49
49
|
from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
|
|
50
|
+
from .node_property_attribute import DisplayLabel, HumanFriendlyIdentifier
|
|
50
51
|
|
|
51
52
|
if TYPE_CHECKING:
|
|
52
53
|
from typing_extensions import Self
|
|
@@ -79,6 +80,29 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
79
80
|
_meta.default_filter = default_filter
|
|
80
81
|
super().__init_subclass_with_meta__(_meta=_meta, **options)
|
|
81
82
|
|
|
83
|
+
def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
|
|
84
|
+
self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
|
|
85
|
+
self._branch: Branch = branch
|
|
86
|
+
self._at: Timestamp = at
|
|
87
|
+
self._existing: bool = False
|
|
88
|
+
|
|
89
|
+
self._updated_at: Timestamp | None = None
|
|
90
|
+
self.id: str = None
|
|
91
|
+
self.db_id: str = None
|
|
92
|
+
|
|
93
|
+
self._source: Node | None = None
|
|
94
|
+
self._owner: Node | None = None
|
|
95
|
+
self._is_protected: bool = None
|
|
96
|
+
self._computed_jinja2_attributes: list[str] = []
|
|
97
|
+
|
|
98
|
+
self._display_label: DisplayLabel | None = None
|
|
99
|
+
self._human_friendly_id: HumanFriendlyIdentifier | None = None
|
|
100
|
+
|
|
101
|
+
# Lists of attributes and relationships names
|
|
102
|
+
self._attributes: list[str] = []
|
|
103
|
+
self._relationships: list[str] = []
|
|
104
|
+
self._node_changelog: NodeChangelog | None = None
|
|
105
|
+
|
|
82
106
|
def get_schema(self) -> NonGenericSchemaTypes:
|
|
83
107
|
return self._schema
|
|
84
108
|
|
|
@@ -126,11 +150,14 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
126
150
|
if not self._schema.human_friendly_id:
|
|
127
151
|
return None
|
|
128
152
|
|
|
129
|
-
hfid_values
|
|
153
|
+
hfid_values: list[str] | None = None
|
|
154
|
+
if self._human_friendly_id:
|
|
155
|
+
hfid_values = self._human_friendly_id.get_value(node=self, at=self._at)
|
|
156
|
+
if not hfid_values:
|
|
157
|
+
hfid_values = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
|
|
158
|
+
|
|
130
159
|
hfid = [value for value in hfid_values if value is not None]
|
|
131
|
-
if include_kind
|
|
132
|
-
return [self.get_kind()] + hfid
|
|
133
|
-
return hfid
|
|
160
|
+
return [self.get_kind()] + hfid if include_kind else hfid
|
|
134
161
|
|
|
135
162
|
async def get_hfid_as_string(self, db: InfrahubDatabase, include_kind: bool = False) -> str | None:
|
|
136
163
|
"""Return the Human friendly id of the node in string format separated with a dunder (__) ."""
|
|
@@ -139,6 +166,37 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
139
166
|
return None
|
|
140
167
|
return "__".join(hfid)
|
|
141
168
|
|
|
169
|
+
def has_human_friendly_id(self) -> bool:
|
|
170
|
+
return self._human_friendly_id is not None
|
|
171
|
+
|
|
172
|
+
async def add_human_friendly_id(self, db: InfrahubDatabase) -> None:
|
|
173
|
+
if not self._schema.human_friendly_id or self._human_friendly_id:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
self._human_friendly_id = HumanFriendlyIdentifier(
|
|
177
|
+
node_schema=self._schema, template=self._schema.human_friendly_id
|
|
178
|
+
)
|
|
179
|
+
await self._human_friendly_id.compute(db=db, node=self)
|
|
180
|
+
|
|
181
|
+
async def get_display_label(self, db: InfrahubDatabase) -> str:
|
|
182
|
+
if self._display_label:
|
|
183
|
+
if isinstance(self._display_label._value, str):
|
|
184
|
+
return self._display_label._value
|
|
185
|
+
if self._display_label._value:
|
|
186
|
+
return self._display_label._value.value
|
|
187
|
+
|
|
188
|
+
return await self.render_display_label(db=db)
|
|
189
|
+
|
|
190
|
+
def has_display_label(self) -> bool:
|
|
191
|
+
return self._display_label is not None
|
|
192
|
+
|
|
193
|
+
async def add_display_label(self, db: InfrahubDatabase) -> None:
|
|
194
|
+
if not self._schema.display_label or self._display_label:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
self._display_label = DisplayLabel(node_schema=self._schema, template=self._schema.display_label)
|
|
198
|
+
await self._display_label.compute(db=db, node=self)
|
|
199
|
+
|
|
142
200
|
async def get_path_value(self, db: InfrahubDatabase, path: str) -> str:
|
|
143
201
|
schema_path = self._schema.parse_schema_path(
|
|
144
202
|
path=path, schema=db.schema.get_schema_branch(name=self._branch.name)
|
|
@@ -197,30 +255,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
197
255
|
return self._branch
|
|
198
256
|
|
|
199
257
|
def __repr__(self) -> str:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return f"{self.get_kind()}(ID: {str(self.id)})"
|
|
204
|
-
|
|
205
|
-
def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
|
|
206
|
-
self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
|
|
207
|
-
self._branch: Branch = branch
|
|
208
|
-
self._at: Timestamp = at
|
|
209
|
-
self._existing: bool = False
|
|
210
|
-
|
|
211
|
-
self._updated_at: Timestamp | None = None
|
|
212
|
-
self.id: str = None
|
|
213
|
-
self.db_id: str = None
|
|
214
|
-
|
|
215
|
-
self._source: Node | None = None
|
|
216
|
-
self._owner: Node | None = None
|
|
217
|
-
self._is_protected: bool = None
|
|
218
|
-
self._computed_jinja2_attributes: list[str] = []
|
|
219
|
-
|
|
220
|
-
# Lists of attributes and relationships names
|
|
221
|
-
self._attributes: list[str] = []
|
|
222
|
-
self._relationships: list[str] = []
|
|
223
|
-
self._node_changelog: NodeChangelog | None = None
|
|
258
|
+
v = f"{self.get_kind()}(ID: {str(self.id)})"
|
|
259
|
+
return v if self._existing else f"{v}[NEW]"
|
|
224
260
|
|
|
225
261
|
@property
|
|
226
262
|
def node_changelog(self) -> NodeChangelog:
|
|
@@ -733,12 +769,26 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
733
769
|
)
|
|
734
770
|
self._updated_at = Timestamp(updated_at)
|
|
735
771
|
|
|
772
|
+
if not self._schema.is_schema_node:
|
|
773
|
+
if hfid := kwargs.pop("human_friendly_id", None):
|
|
774
|
+
self._human_friendly_id = HumanFriendlyIdentifier(
|
|
775
|
+
node_schema=self._schema, template=self._schema.human_friendly_id, value=hfid
|
|
776
|
+
)
|
|
777
|
+
if display_label := kwargs.pop("display_label", None):
|
|
778
|
+
self._display_label = DisplayLabel(
|
|
779
|
+
node_schema=self._schema, template=self._schema.display_label, value=display_label
|
|
780
|
+
)
|
|
781
|
+
|
|
736
782
|
await self._process_fields(db=db, fields=kwargs)
|
|
737
783
|
return self
|
|
738
784
|
|
|
739
785
|
async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> NodeChangelog:
|
|
740
786
|
create_at = Timestamp(at)
|
|
741
787
|
|
|
788
|
+
if not self._schema.is_schema_node:
|
|
789
|
+
await self.add_human_friendly_id(db=db)
|
|
790
|
+
await self.add_display_label(db=db)
|
|
791
|
+
|
|
742
792
|
query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at)
|
|
743
793
|
await query.execute(db=db)
|
|
744
794
|
|
|
@@ -750,6 +800,13 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
750
800
|
new_ids = query.get_ids()
|
|
751
801
|
node_changelog = NodeChangelog(node_id=self.get_id(), node_kind=self.get_kind(), display_label="")
|
|
752
802
|
|
|
803
|
+
if self._human_friendly_id:
|
|
804
|
+
node_changelog.create_attribute(
|
|
805
|
+
attribute=self._human_friendly_id.get_node_attribute(node=self, at=create_at)
|
|
806
|
+
)
|
|
807
|
+
if self._display_label:
|
|
808
|
+
node_changelog.create_attribute(attribute=self._display_label.get_node_attribute(node=self, at=create_at))
|
|
809
|
+
|
|
753
810
|
# Go over the list of Attribute and assign the new IDs one by one
|
|
754
811
|
for name in self._attributes:
|
|
755
812
|
attr: BaseAttribute = getattr(self, name)
|
|
@@ -762,12 +819,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
762
819
|
relm: RelationshipManager = getattr(self, name)
|
|
763
820
|
for rel in relm._relationships:
|
|
764
821
|
identifier = f"{rel.schema.identifier}::{rel.peer_id}"
|
|
765
|
-
|
|
766
822
|
rel.id, rel.db_id = new_ids[identifier]
|
|
767
|
-
|
|
768
823
|
node_changelog.create_relationship(relationship=rel)
|
|
769
824
|
|
|
770
|
-
node_changelog.display_label = await self.
|
|
825
|
+
node_changelog.display_label = await self.get_display_label(db=db)
|
|
771
826
|
return node_changelog
|
|
772
827
|
|
|
773
828
|
async def _update(
|
|
@@ -803,19 +858,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
803
858
|
if parent := await rel.get_parent(db=db):
|
|
804
859
|
node_changelog.add_parent_from_relationship(parent=parent)
|
|
805
860
|
|
|
806
|
-
|
|
861
|
+
# Update the HFID if one of its variables is being updated
|
|
862
|
+
if self._human_friendly_id and (
|
|
863
|
+
(fields and "human_friendly_id" in fields) or self._human_friendly_id.needs_update(fields=fields)
|
|
864
|
+
):
|
|
865
|
+
await self._human_friendly_id.compute(db=db, node=self)
|
|
866
|
+
updated_attribute = await self._human_friendly_id.get_node_attribute(node=self, at=update_at).save(
|
|
867
|
+
at=update_at, db=db
|
|
868
|
+
)
|
|
869
|
+
if updated_attribute:
|
|
870
|
+
node_changelog.add_attribute(attribute=updated_attribute)
|
|
871
|
+
|
|
872
|
+
# Update the display label if one of its variables is being updated
|
|
873
|
+
if self._display_label and (
|
|
874
|
+
(fields and "display_label" in fields) or self._display_label.needs_update(fields=fields)
|
|
875
|
+
):
|
|
876
|
+
await self._display_label.compute(db=db, node=self)
|
|
877
|
+
self._display_label.get_node_attribute(node=self, at=update_at).get_create_data(node_schema=self._schema)
|
|
878
|
+
updated_attribute = await self._display_label.get_node_attribute(node=self, at=update_at).save(
|
|
879
|
+
at=update_at, db=db
|
|
880
|
+
)
|
|
881
|
+
if updated_attribute:
|
|
882
|
+
node_changelog.add_attribute(attribute=updated_attribute)
|
|
883
|
+
|
|
884
|
+
node_changelog.display_label = await self.get_display_label(db=db)
|
|
807
885
|
return node_changelog
|
|
808
886
|
|
|
809
887
|
async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> Self:
|
|
810
888
|
"""Create or Update the Node in the database."""
|
|
811
|
-
|
|
812
889
|
save_at = Timestamp(at)
|
|
813
890
|
|
|
814
891
|
if self._existing:
|
|
815
892
|
self._node_changelog = await self._update(at=save_at, db=db, fields=fields)
|
|
816
|
-
|
|
893
|
+
else:
|
|
894
|
+
self._node_changelog = await self._create(at=save_at, db=db)
|
|
817
895
|
|
|
818
|
-
self._node_changelog = await self._create(at=save_at, db=db)
|
|
819
896
|
return self
|
|
820
897
|
|
|
821
898
|
async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
|
|
@@ -824,13 +901,24 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
824
901
|
delete_at = Timestamp(at)
|
|
825
902
|
|
|
826
903
|
node_changelog = NodeChangelog(
|
|
827
|
-
node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.
|
|
904
|
+
node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.get_display_label(db=db)
|
|
828
905
|
)
|
|
829
906
|
# Go over the list of Attribute and update them one by one
|
|
830
907
|
for name in self._attributes:
|
|
831
908
|
attr: BaseAttribute = getattr(self, name)
|
|
832
|
-
deleted_attribute
|
|
833
|
-
|
|
909
|
+
if deleted_attribute := await attr.delete(at=delete_at, db=db):
|
|
910
|
+
node_changelog.add_attribute(attribute=deleted_attribute)
|
|
911
|
+
|
|
912
|
+
if self._human_friendly_id:
|
|
913
|
+
if deleted_attribute := await self._human_friendly_id.get_node_attribute(node=self, at=delete_at).delete(
|
|
914
|
+
at=delete_at, db=db
|
|
915
|
+
):
|
|
916
|
+
node_changelog.add_attribute(attribute=deleted_attribute)
|
|
917
|
+
|
|
918
|
+
if self._display_label:
|
|
919
|
+
if deleted_attribute := await self._display_label.get_node_attribute(node=self, at=delete_at).delete(
|
|
920
|
+
at=delete_at, db=db
|
|
921
|
+
):
|
|
834
922
|
node_changelog.add_attribute(attribute=deleted_attribute)
|
|
835
923
|
|
|
836
924
|
branch = self.get_branch_based_on_support_type()
|
|
@@ -898,7 +986,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
898
986
|
continue
|
|
899
987
|
|
|
900
988
|
if field_name == "display_label":
|
|
901
|
-
response[field_name] = await self.
|
|
989
|
+
response[field_name] = await self.get_display_label(db=db)
|
|
902
990
|
continue
|
|
903
991
|
|
|
904
992
|
if field_name == "hfid":
|
|
@@ -1001,6 +1089,20 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
1001
1089
|
return repr(self)
|
|
1002
1090
|
return display_label.strip()
|
|
1003
1091
|
|
|
1092
|
+
async def set_human_friendly_id(self, value: list[str] | None) -> None:
|
|
1093
|
+
"""Set the human friendly ID of this node if one is set. `save()` must be called to commit the change in the database."""
|
|
1094
|
+
if self._human_friendly_id is None:
|
|
1095
|
+
return
|
|
1096
|
+
|
|
1097
|
+
self._human_friendly_id.set_value(value=value, manually_assigned=True)
|
|
1098
|
+
|
|
1099
|
+
async def set_display_label(self, value: str | None) -> None:
|
|
1100
|
+
"""Set the display label of this node if one is set. `save()` must be called to commit the change in the database."""
|
|
1101
|
+
if self._display_label is None:
|
|
1102
|
+
return
|
|
1103
|
+
|
|
1104
|
+
self._display_label.set_value(value=value, manually_assigned=True)
|
|
1105
|
+
|
|
1004
1106
|
def _get_parent_relationship_name(self) -> str | None:
|
|
1005
1107
|
"""Return the name of the parent relationship is one is present"""
|
|
1006
1108
|
for relationship in self._schema.relationships:
|
|
@@ -1010,7 +1112,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
1010
1112
|
return None
|
|
1011
1113
|
|
|
1012
1114
|
async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
|
|
1013
|
-
object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
|
|
1115
|
+
object_template: RelationshipManager | None = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
|
|
1014
1116
|
return (
|
|
1015
1117
|
await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
|
|
1016
1118
|
)
|
|
@@ -29,7 +29,9 @@ class NodeAttributeUniquenessConstraint(NodeConstraintInterface):
|
|
|
29
29
|
attr = getattr(node, unique_attr.name)
|
|
30
30
|
if unique_attr.inherited:
|
|
31
31
|
for generic_parent_schema_name in node_schema.inherit_from:
|
|
32
|
-
generic_parent_schema = self.db.schema.get(
|
|
32
|
+
generic_parent_schema = self.db.schema.get(
|
|
33
|
+
generic_parent_schema_name, branch=self.branch, duplicate=False
|
|
34
|
+
)
|
|
33
35
|
parent_attr = generic_parent_schema.get_attribute_or_none(unique_attr.name)
|
|
34
36
|
if parent_attr is None:
|
|
35
37
|
continue
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from infrahub_sdk.template import Jinja2Template
|
|
8
|
+
|
|
9
|
+
from infrahub.core.query.node import AttributeFromDB
|
|
10
|
+
from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
|
|
11
|
+
|
|
12
|
+
from ..attribute import BaseAttribute, ListAttributeOptional, StringOptional
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from infrahub.core.node import Node
|
|
16
|
+
from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
|
|
17
|
+
from infrahub.core.schema.attribute_schema import AttributeSchema
|
|
18
|
+
from infrahub.core.timestamp import Timestamp
|
|
19
|
+
from infrahub.database import InfrahubDatabase
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NodePropertyAttribute(Generic[T]):
|
|
25
|
+
"""A node property attribute is a construct that seats between a property and an attribute.
|
|
26
|
+
|
|
27
|
+
View it as a property, set at the node level but stored in the database as an attribute. It usually is something computed from other components of
|
|
28
|
+
a node, such as its attributes and its relationships.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
node_schema: NodeSchema | ProfileSchema | TemplateSchema,
|
|
34
|
+
template: T | None,
|
|
35
|
+
value: AttributeFromDB | T | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.node_schema = node_schema
|
|
38
|
+
|
|
39
|
+
self.node_attributes: list[str] = []
|
|
40
|
+
self.node_relationships: list[str] = []
|
|
41
|
+
|
|
42
|
+
self.template = template
|
|
43
|
+
self._value = value
|
|
44
|
+
self._manually_assigned = False
|
|
45
|
+
|
|
46
|
+
self.schema: AttributeSchema
|
|
47
|
+
|
|
48
|
+
self.analyze_variables()
|
|
49
|
+
|
|
50
|
+
def needs_update(self, fields: list[str] | None) -> bool:
|
|
51
|
+
"""Tell if this node property attribute must be recomputed given a list of updated fields of a node."""
|
|
52
|
+
if self._manually_assigned or not fields:
|
|
53
|
+
return True
|
|
54
|
+
for field in fields:
|
|
55
|
+
if field in self.node_attributes or field in self.node_relationships:
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def attribute_value(self) -> AttributeFromDB | dict[str, T | None]:
|
|
62
|
+
if isinstance(self._value, AttributeFromDB):
|
|
63
|
+
return self._value
|
|
64
|
+
return {"value": self._value}
|
|
65
|
+
|
|
66
|
+
def set_value(self, value: T | None, manually_assigned: bool = False) -> None:
|
|
67
|
+
"""Force the value of the node property attribute to the given one."""
|
|
68
|
+
if isinstance(self._value, AttributeFromDB):
|
|
69
|
+
self._value.value = value
|
|
70
|
+
else:
|
|
71
|
+
self._value = value
|
|
72
|
+
|
|
73
|
+
if manually_assigned:
|
|
74
|
+
self._manually_assigned = True
|
|
75
|
+
|
|
76
|
+
def get_value(self, node: Node, at: Timestamp) -> T | None:
|
|
77
|
+
if isinstance(self._value, AttributeFromDB):
|
|
78
|
+
attr = self.get_node_attribute(node=node, at=at)
|
|
79
|
+
return attr.value # type: ignore
|
|
80
|
+
|
|
81
|
+
return self._value
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def analyze_variables(self) -> None: ...
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
async def compute(self, db: InfrahubDatabase, node: Node) -> None: ...
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def get_node_attribute(self, node: Node, at: Timestamp) -> BaseAttribute: ...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DisplayLabel(NodePropertyAttribute[str]):
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
node_schema: NodeSchema | ProfileSchema | TemplateSchema,
|
|
97
|
+
template: str | None,
|
|
98
|
+
value: AttributeFromDB | str | None = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
super().__init__(node_schema=node_schema, template=template, value=value)
|
|
101
|
+
|
|
102
|
+
self.schema = node_schema.get_attribute(name="display_label")
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def is_jinja2_template(self) -> bool:
|
|
106
|
+
if self.template is None:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return any(c in self.template for c in "{}")
|
|
110
|
+
|
|
111
|
+
def _analyze_plain_value(self) -> None:
|
|
112
|
+
if self.template is None or "__" not in self.template:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
items = self.template.split("__", maxsplit=1)
|
|
116
|
+
if items[0] not in self.node_schema.attribute_names:
|
|
117
|
+
raise ValueError(f"{items[0]} is not an attribute of {self.node_schema.kind}")
|
|
118
|
+
|
|
119
|
+
self.node_attributes.append(items[0])
|
|
120
|
+
|
|
121
|
+
def _analyze_jinja2_value(self) -> None:
|
|
122
|
+
if self.template is None or not self.is_jinja2_template:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
tpl = Jinja2Template(template=self.template)
|
|
126
|
+
for variable in tpl.get_variables():
|
|
127
|
+
items = variable.split("__", maxsplit=1)
|
|
128
|
+
if items[0] in self.node_schema.attribute_names:
|
|
129
|
+
self.node_attributes.append(items[0])
|
|
130
|
+
elif items[0] in self.node_schema.relationship_names:
|
|
131
|
+
self.node_relationships.append(items[0])
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
|
|
134
|
+
|
|
135
|
+
def analyze_variables(self) -> None:
|
|
136
|
+
"""Look at variables used in the display label and record attributes and relationships required to compute it."""
|
|
137
|
+
if not self.is_jinja2_template:
|
|
138
|
+
self._analyze_plain_value()
|
|
139
|
+
else:
|
|
140
|
+
self._analyze_jinja2_value()
|
|
141
|
+
|
|
142
|
+
async def compute(self, db: InfrahubDatabase, node: Node) -> None:
|
|
143
|
+
"""Update the display label value by recomputing it from the template."""
|
|
144
|
+
if self.template is None or self._manually_assigned:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if node.get_schema() != self.node_schema:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"display_label for schema {self.node_schema.kind} cannot be rendered for node {node.get_schema().kind} {node.id}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not self.is_jinja2_template:
|
|
153
|
+
path_value = await node.get_path_value(db=db, path=self.template)
|
|
154
|
+
# Use .value for enum to keep compat with old style display label
|
|
155
|
+
self.set_value(value=str(path_value if not isinstance(path_value, Enum) else path_value.value))
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
jinja2_template = Jinja2Template(template=self.template)
|
|
159
|
+
|
|
160
|
+
variables: dict[str, Any] = {}
|
|
161
|
+
for variable in jinja2_template.get_variables():
|
|
162
|
+
variables[variable] = await node.get_path_value(db=db, path=variable)
|
|
163
|
+
|
|
164
|
+
self.set_value(value=await jinja2_template.render(variables=variables))
|
|
165
|
+
|
|
166
|
+
def get_node_attribute(self, node: Node, at: Timestamp) -> StringOptional:
|
|
167
|
+
"""Return a node attribute that can be stored in the database for this display label and node."""
|
|
168
|
+
return StringOptional(
|
|
169
|
+
name="display_label",
|
|
170
|
+
schema=self.schema,
|
|
171
|
+
branch=node.get_branch(),
|
|
172
|
+
at=at,
|
|
173
|
+
node=node,
|
|
174
|
+
data=self.attribute_value,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class HumanFriendlyIdentifier(NodePropertyAttribute[list[str]]):
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
node_schema: NodeSchema | ProfileSchema | TemplateSchema,
|
|
182
|
+
template: list[str] | None,
|
|
183
|
+
value: AttributeFromDB | list[str] | None = None,
|
|
184
|
+
) -> None:
|
|
185
|
+
super().__init__(node_schema=node_schema, template=template, value=value)
|
|
186
|
+
|
|
187
|
+
self.schema = node_schema.get_attribute(name="human_friendly_id")
|
|
188
|
+
|
|
189
|
+
def _analyze_single_variable(self, value: str) -> None:
|
|
190
|
+
items = value.split("__", maxsplit=1)
|
|
191
|
+
if items[0] in self.node_schema.attribute_names:
|
|
192
|
+
self.node_attributes.append(items[0])
|
|
193
|
+
elif items[0] in self.node_schema.relationship_names:
|
|
194
|
+
self.node_relationships.append(items[0])
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
|
|
197
|
+
|
|
198
|
+
def analyze_variables(self) -> None:
|
|
199
|
+
"""Look at variables used in the HFID and record attributes and relationships required to compute it."""
|
|
200
|
+
for item in self.template or []:
|
|
201
|
+
self._analyze_single_variable(value=item)
|
|
202
|
+
|
|
203
|
+
async def compute(self, db: InfrahubDatabase, node: Node) -> None:
|
|
204
|
+
"""Update the HFID value by recomputing it from the template."""
|
|
205
|
+
if self.template is None or self._manually_assigned:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if node.get_schema() != self.node_schema:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"human_friendly_id for schema {self.node_schema.kind} cannot be computed for node {node.get_schema().kind} {node.id}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
value: list[str] = []
|
|
214
|
+
for path in self.template:
|
|
215
|
+
path_value = await node.get_path_value(db=db, path=path)
|
|
216
|
+
# Use .value for enum to be consistent with display label
|
|
217
|
+
value.append(path_value if not isinstance(path_value, Enum) else path_value.value)
|
|
218
|
+
|
|
219
|
+
self.set_value(value=value)
|
|
220
|
+
|
|
221
|
+
def get_node_attribute(self, node: Node, at: Timestamp) -> ListAttributeOptional:
|
|
222
|
+
"""Return a node attribute that can be stored in the database for this HFID and node."""
|
|
223
|
+
return ListAttributeOptional(
|
|
224
|
+
name="human_friendly_id",
|
|
225
|
+
schema=self.schema,
|
|
226
|
+
branch=node.get_branch(),
|
|
227
|
+
at=at,
|
|
228
|
+
node=node,
|
|
229
|
+
data=self.attribute_value,
|
|
230
|
+
)
|
infrahub/core/node/standard.py
CHANGED
infrahub/core/protocols.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from .protocols_base import CoreNode
|
|
7
|
+
from infrahub.core.protocols_base import CoreNode
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from enum import Enum
|
|
@@ -350,6 +350,10 @@ class CoreGeneratorAction(CoreAction):
|
|
|
350
350
|
generator: RelationshipManager
|
|
351
351
|
|
|
352
352
|
|
|
353
|
+
class CoreGeneratorAwareGroup(CoreGroup):
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
|
|
353
357
|
class CoreGeneratorCheck(CoreCheck):
|
|
354
358
|
instance: String
|
|
355
359
|
|
|
@@ -361,6 +365,8 @@ class CoreGeneratorDefinition(CoreTaskTarget):
|
|
|
361
365
|
file_path: String
|
|
362
366
|
class_name: String
|
|
363
367
|
convert_query_response: BooleanOptional
|
|
368
|
+
execute_in_proposed_change: BooleanOptional
|
|
369
|
+
execute_after_merge: BooleanOptional
|
|
364
370
|
query: RelationshipManager
|
|
365
371
|
repository: RelationshipManager
|
|
366
372
|
targets: RelationshipManager
|
infrahub/core/query/node.py
CHANGED
|
@@ -142,9 +142,22 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
142
142
|
attributes_ipnetwork: list[AttributeCreateData] = []
|
|
143
143
|
attributes_indexed: list[AttributeCreateData] = []
|
|
144
144
|
|
|
145
|
+
if self.node.has_display_label():
|
|
146
|
+
attributes_indexed.append(
|
|
147
|
+
self.node._display_label.get_node_attribute(node=self.node, at=at).get_create_data(
|
|
148
|
+
node_schema=self.node.get_schema()
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
if self.node.has_human_friendly_id():
|
|
152
|
+
attributes_indexed.append(
|
|
153
|
+
self.node._human_friendly_id.get_node_attribute(node=self.node, at=at).get_create_data(
|
|
154
|
+
node_schema=self.node.get_schema()
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
145
158
|
for attr_name in self.node._attributes:
|
|
146
159
|
attr: BaseAttribute = getattr(self.node, attr_name)
|
|
147
|
-
attr_data = attr.get_create_data()
|
|
160
|
+
attr_data = attr.get_create_data(node_schema=self.node.get_schema())
|
|
148
161
|
node_type = attr.get_db_node_type()
|
|
149
162
|
|
|
150
163
|
if AttributeDBNodeType.IPHOST in node_type:
|
infrahub/core/registry.py
CHANGED
|
@@ -113,8 +113,8 @@ class Registry:
|
|
|
113
113
|
return True
|
|
114
114
|
return False
|
|
115
115
|
|
|
116
|
-
def get_node_schema(self, name: str, branch: Branch | str | None = None) -> NodeSchema:
|
|
117
|
-
return self.schema.get_node_schema(name=name, branch=branch)
|
|
116
|
+
def get_node_schema(self, name: str, branch: Branch | str | None = None, duplicate: bool = False) -> NodeSchema:
|
|
117
|
+
return self.schema.get_node_schema(name=name, branch=branch, duplicate=duplicate)
|
|
118
118
|
|
|
119
119
|
def get_data_type(self, name: str) -> type[InfrahubDataType]:
|
|
120
120
|
if name not in self.data_type:
|
|
@@ -40,7 +40,7 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
|
|
|
40
40
|
# peer_ids_present_database_only:
|
|
41
41
|
# relationship to be deleted, need to check if the schema on the other side has a min_count defined
|
|
42
42
|
# TODO see how to manage Generic node
|
|
43
|
-
peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch)
|
|
43
|
+
peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch, duplicate=False)
|
|
44
44
|
peer_rels = peer_schema.get_relationships_by_identifier(id=relm.schema.get_identifier())
|
|
45
45
|
if not peer_rels:
|
|
46
46
|
return
|
|
@@ -459,7 +459,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
459
459
|
self.set_peer(value=peer)
|
|
460
460
|
|
|
461
461
|
if not self.peer_id and self.peer_hfid:
|
|
462
|
-
peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch)
|
|
462
|
+
peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch, duplicate=False)
|
|
463
463
|
kind = (
|
|
464
464
|
self.data["kind"]
|
|
465
465
|
if isinstance(self.data, dict) and "kind" in self.data and peer_schema.is_generic_schema
|