infrahub-server 1.3.3__py3-none-any.whl → 1.3.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.
- infrahub/api/schema.py +2 -2
- infrahub/cli/db.py +34 -0
- infrahub/core/convert_object_type/conversion.py +10 -0
- infrahub/core/diff/enricher/hierarchy.py +7 -3
- infrahub/core/diff/query_parser.py +7 -3
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +84 -0
- infrahub/core/migrations/query/node_duplicate.py +5 -1
- infrahub/core/migrations/schema/node_attribute_add.py +55 -2
- infrahub/core/migrations/shared.py +37 -9
- infrahub/core/node/__init__.py +41 -21
- infrahub/core/node/resource_manager/number_pool.py +60 -22
- infrahub/core/query/diff.py +17 -3
- infrahub/core/query/relationship.py +8 -11
- infrahub/core/query/resource_manager.py +117 -20
- infrahub/core/schema/__init__.py +5 -0
- infrahub/core/schema/attribute_parameters.py +6 -0
- infrahub/core/schema/attribute_schema.py +6 -0
- infrahub/core/schema/manager.py +5 -11
- infrahub/core/schema/relationship_schema.py +6 -0
- infrahub/core/schema/schema_branch.py +72 -11
- infrahub/core/validators/node/attribute.py +15 -0
- infrahub/core/validators/tasks.py +12 -4
- infrahub/generators/tasks.py +1 -1
- infrahub/git/integrator.py +1 -1
- infrahub/git/tasks.py +2 -2
- infrahub/graphql/mutations/main.py +24 -5
- infrahub/graphql/queries/resource_manager.py +4 -4
- infrahub/proposed_change/tasks.py +2 -2
- infrahub/tasks/registry.py +63 -35
- infrahub_sdk/client.py +7 -8
- infrahub_sdk/ctl/utils.py +3 -0
- infrahub_sdk/node/node.py +87 -15
- infrahub_sdk/node/relationship.py +43 -2
- infrahub_sdk/protocols_base.py +0 -2
- infrahub_sdk/protocols_generator/constants.py +1 -0
- infrahub_sdk/utils.py +0 -17
- infrahub_sdk/yaml.py +13 -7
- infrahub_server-1.3.5.dist-info/LICENSE.txt +201 -0
- {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/METADATA +3 -3
- {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/RECORD +44 -43
- infrahub_server-1.3.3.dist-info/LICENSE.txt +0 -661
- {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/WHEEL +0 -0
- {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/entry_points.txt +0 -0
infrahub/core/query/diff.py
CHANGED
|
@@ -148,7 +148,14 @@ CALL (diff_path) {
|
|
|
148
148
|
OR type(base_r_node) <> "IS_RELATED" OR type(base_r_prop) <> "IS_RELATED"
|
|
149
149
|
)
|
|
150
150
|
WITH latest_base_path, base_r_root, base_r_node, base_r_prop
|
|
151
|
-
|
|
151
|
+
// status="active" ordering is for tie-breaking edges added and deleted at the same time, we want the active one
|
|
152
|
+
ORDER BY
|
|
153
|
+
base_r_prop.from DESC,
|
|
154
|
+
base_r_prop.status = "active" DESC,
|
|
155
|
+
base_r_node.from DESC,
|
|
156
|
+
base_r_node.status = "active" DESC,
|
|
157
|
+
base_r_root.from DESC,
|
|
158
|
+
base_r_root.status = "active" DESC
|
|
152
159
|
LIMIT 1
|
|
153
160
|
RETURN latest_base_path
|
|
154
161
|
}
|
|
@@ -172,8 +179,6 @@ CALL (penultimate_path) {
|
|
|
172
179
|
AND %(id_func)s(peer_r_node) = %(id_func)s(r_node)
|
|
173
180
|
AND [%(id_func)s(n), type(peer_r_node)] <> [%(id_func)s(peer), type(r_peer)]
|
|
174
181
|
AND r_peer.from < $to_time
|
|
175
|
-
// filter out paths where an earlier from time follows a later from time
|
|
176
|
-
AND peer_r_node.from <= r_peer.from
|
|
177
182
|
// filter out paths where a base branch edge follows a branch edge
|
|
178
183
|
AND (peer_r_node.branch = $base_branch_name OR r_peer.branch = $branch_name)
|
|
179
184
|
// filter out paths where an active edge follows a deleted edge
|
|
@@ -663,6 +668,15 @@ AND ALL(
|
|
|
663
668
|
AND ((r_pair[0]).status = "active" OR (r_pair[1]).status = "deleted")
|
|
664
669
|
// filter out paths where an earlier from time follows a later from time
|
|
665
670
|
AND (r_pair[0]).from <= (r_pair[1]).from
|
|
671
|
+
// if both are deleted, then the deeper edge must have been deleted first
|
|
672
|
+
AND ((r_pair[0]).status = "active" OR (r_pair[1]).status = "active" OR (r_pair[0]).from >= (r_pair[1].from))
|
|
673
|
+
AND (
|
|
674
|
+
(r_pair[0]).status = (r_pair[1]).status
|
|
675
|
+
OR (
|
|
676
|
+
(r_pair[0]).from <= (r_pair[1]).from
|
|
677
|
+
AND ((r_pair[0]).to IS NULL OR (r_pair[0]).to >= (r_pair[1]).from)
|
|
678
|
+
)
|
|
679
|
+
)
|
|
666
680
|
// require adjacent edge pairs to have overlapping times, but only if on the same branch
|
|
667
681
|
AND (
|
|
668
682
|
(r_pair[0]).branch <> (r_pair[1]).branch
|
|
@@ -643,12 +643,9 @@ class RelationshipGetPeerQuery(Query):
|
|
|
643
643
|
|
|
644
644
|
arrows = self.schema.get_query_arrows()
|
|
645
645
|
|
|
646
|
-
path_str = (
|
|
647
|
-
f"{arrows.left.start}[:IS_RELATED]{arrows.left.end}(rl){arrows.right.start}[:IS_RELATED]{arrows.right.end}"
|
|
648
|
-
)
|
|
646
|
+
path_str = f"{arrows.left.start}[r1:IS_RELATED]{arrows.left.end}(rl){arrows.right.start}[r2:IS_RELATED]{arrows.right.end}"
|
|
649
647
|
|
|
650
648
|
branch_level_str = "reduce(br_lvl = 0, r in relationships(path) | br_lvl + r.branch_level)"
|
|
651
|
-
froms_str = db.render_list_comprehension(items="relationships(path)", item_name="from")
|
|
652
649
|
query = """
|
|
653
650
|
MATCH (source_node:Node)%(arrow_left_start)s[:IS_RELATED]%(arrow_left_end)s(rl:Relationship { name: $rel_identifier })
|
|
654
651
|
WHERE source_node.uuid IN $source_ids
|
|
@@ -659,24 +656,24 @@ class RelationshipGetPeerQuery(Query):
|
|
|
659
656
|
$source_kind IN LABELS(source_node) AND
|
|
660
657
|
peer.uuid <> source_node.uuid AND
|
|
661
658
|
$peer_kind IN LABELS(peer) AND
|
|
662
|
-
all(r IN
|
|
663
|
-
WITH source_node, peer, rl,
|
|
664
|
-
RETURN peer as peer,
|
|
665
|
-
|
|
659
|
+
all(r IN [r1, r2] WHERE (%(branch_filter)s))
|
|
660
|
+
WITH source_node, peer, rl, r1, r2, %(branch_level)s AS branch_level
|
|
661
|
+
RETURN peer as peer, r1.status = "active" AND r2.status = "active" AS is_active, [r1, r2] AS rels
|
|
662
|
+
// status is required as a tiebreaker for migrated-kind nodes
|
|
663
|
+
ORDER BY branch_level DESC, r2.from DESC, r2.status ASC, r1.from DESC, r1.status ASC
|
|
666
664
|
LIMIT 1
|
|
667
665
|
}
|
|
668
|
-
WITH peer,
|
|
666
|
+
WITH peer, rl, is_active, rels, source_node
|
|
669
667
|
""" % {
|
|
670
668
|
"path": path_str,
|
|
671
669
|
"branch_filter": branch_filter,
|
|
672
670
|
"branch_level": branch_level_str,
|
|
673
|
-
"froms": froms_str,
|
|
674
671
|
"arrow_left_start": arrows.left.start,
|
|
675
672
|
"arrow_left_end": arrows.left.end,
|
|
676
673
|
}
|
|
677
674
|
|
|
678
675
|
self.add_to_query(query)
|
|
679
|
-
where_clause = [
|
|
676
|
+
where_clause = ["is_active = TRUE"]
|
|
680
677
|
clean_filters = extract_field_filters(field_name=self.schema.name, filters=self.filters)
|
|
681
678
|
|
|
682
679
|
if (clean_filters and "id" in clean_filters) or "ids" in clean_filters:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Generator
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
6
|
|
|
5
7
|
from infrahub.core import registry
|
|
6
8
|
from infrahub.core.constants import InfrahubKind, RelationshipStatus
|
|
@@ -11,6 +13,13 @@ if TYPE_CHECKING:
|
|
|
11
13
|
from infrahub.database import InfrahubDatabase
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
class NumberPoolIdentifierData(BaseModel):
|
|
17
|
+
model_config = ConfigDict(frozen=True)
|
|
18
|
+
|
|
19
|
+
value: int
|
|
20
|
+
identifier: str
|
|
21
|
+
|
|
22
|
+
|
|
14
23
|
class IPAddressPoolGetIdentifiers(Query):
|
|
15
24
|
name = "ipaddresspool_get_identifiers"
|
|
16
25
|
type = QueryType.READ
|
|
@@ -158,7 +167,7 @@ class NumberPoolGetReserved(Query):
|
|
|
158
167
|
def __init__(
|
|
159
168
|
self,
|
|
160
169
|
pool_id: str,
|
|
161
|
-
identifier: str,
|
|
170
|
+
identifier: str | None = None,
|
|
162
171
|
**kwargs: dict[str, Any],
|
|
163
172
|
) -> None:
|
|
164
173
|
self.pool_id = pool_id
|
|
@@ -176,26 +185,104 @@ class NumberPoolGetReserved(Query):
|
|
|
176
185
|
|
|
177
186
|
self.params.update(branch_params)
|
|
178
187
|
|
|
188
|
+
# If identifier is not provided, we return all reservations for the pool
|
|
189
|
+
identifier_filter = ""
|
|
190
|
+
if self.identifier:
|
|
191
|
+
identifier_filter = "r.identifier = $identifier AND "
|
|
192
|
+
self.params["identifier"] = self.identifier
|
|
193
|
+
|
|
179
194
|
query = """
|
|
180
195
|
MATCH (pool:%(number_pool)s { uuid: $pool_id })-[r:IS_RESERVED]->(reservation:AttributeValue)
|
|
181
196
|
WHERE
|
|
182
|
-
|
|
183
|
-
AND
|
|
197
|
+
%(identifier_filter)s
|
|
184
198
|
%(branch_filter)s
|
|
185
|
-
""" % {
|
|
199
|
+
""" % {
|
|
200
|
+
"branch_filter": branch_filter,
|
|
201
|
+
"number_pool": InfrahubKind.NUMBERPOOL,
|
|
202
|
+
"identifier_filter": identifier_filter,
|
|
203
|
+
}
|
|
186
204
|
self.add_to_query(query)
|
|
187
|
-
self.return_labels = ["reservation.value"]
|
|
205
|
+
self.return_labels = ["reservation.value AS value", "r.identifier AS identifier"]
|
|
188
206
|
|
|
189
207
|
def get_reservation(self) -> int | None:
|
|
190
208
|
result = self.get_result()
|
|
191
209
|
if result:
|
|
192
|
-
return result.get_as_optional_type("
|
|
210
|
+
return result.get_as_optional_type("value", return_type=int)
|
|
193
211
|
return None
|
|
194
212
|
|
|
213
|
+
def get_reservations(self) -> Generator[NumberPoolIdentifierData]:
|
|
214
|
+
for result in self.results:
|
|
215
|
+
yield NumberPoolIdentifierData.model_construct(
|
|
216
|
+
value=result.get_as_type("value", return_type=int),
|
|
217
|
+
identifier=result.get_as_type("identifier", return_type=str),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class PoolChangeReserved(Query):
|
|
222
|
+
"""Change the identifier on all pools.
|
|
223
|
+
This is useful when a node is being converted to a different type and its ID has changed
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
name = "pool_change_reserved"
|
|
227
|
+
type = QueryType.WRITE
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
existing_identifier: str,
|
|
232
|
+
new_identifier: str,
|
|
233
|
+
**kwargs: dict[str, Any],
|
|
234
|
+
) -> None:
|
|
235
|
+
self.existing_identifier = existing_identifier
|
|
236
|
+
self.new_identifier = new_identifier
|
|
237
|
+
|
|
238
|
+
super().__init__(**kwargs) # type: ignore[arg-type]
|
|
239
|
+
|
|
240
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
241
|
+
self.params["new_identifier"] = self.new_identifier
|
|
242
|
+
self.params["existing_identifier"] = self.existing_identifier
|
|
243
|
+
self.params["at"] = self.at.to_string()
|
|
244
|
+
|
|
245
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
246
|
+
at=self.at.to_string(), branch_agnostic=self.branch_agnostic
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.params.update(branch_params)
|
|
250
|
+
|
|
251
|
+
global_branch = registry.get_global_branch()
|
|
252
|
+
self.params["rel_prop"] = {
|
|
253
|
+
"branch": global_branch.name,
|
|
254
|
+
"branch_level": global_branch.hierarchy_level,
|
|
255
|
+
"status": RelationshipStatus.ACTIVE.value,
|
|
256
|
+
"from": self.at.to_string(),
|
|
257
|
+
"identifier": self.new_identifier,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
query = """
|
|
261
|
+
MATCH (pool:Node)-[r:IS_RESERVED]->(resource)
|
|
262
|
+
WHERE
|
|
263
|
+
r.identifier = $existing_identifier
|
|
264
|
+
AND
|
|
265
|
+
%(branch_filter)s
|
|
266
|
+
SET r.to = $at
|
|
267
|
+
CREATE (pool)-[new_rel:IS_RESERVED $rel_prop]->(resource)
|
|
268
|
+
""" % {"branch_filter": branch_filter}
|
|
269
|
+
self.add_to_query(query)
|
|
270
|
+
self.return_labels = ["pool.uuid AS pool_id", "r", "new_rel"]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
Important!: The relationship IS_RESERVED for Number is not being cleaned up when the node or the branch is deleted
|
|
275
|
+
I think this is something we should address in the future.
|
|
276
|
+
It works for now because the query has been updated to match the identifier in IS_RESERVED with the UUID of the related node
|
|
277
|
+
But in the future, if we need to use an identifier that is not the UUID, we will need to clean up the relationships
|
|
278
|
+
This will be especially important as we want to support upsert with NumberPool
|
|
279
|
+
"""
|
|
280
|
+
|
|
195
281
|
|
|
196
282
|
class NumberPoolGetUsed(Query):
|
|
197
283
|
name = "number_pool_get_used"
|
|
198
284
|
type = QueryType.READ
|
|
285
|
+
return_model = NumberPoolIdentifierData
|
|
199
286
|
|
|
200
287
|
def __init__(
|
|
201
288
|
self,
|
|
@@ -219,26 +306,36 @@ class NumberPoolGetUsed(Query):
|
|
|
219
306
|
self.params["attribute_name"] = self.pool.node_attribute.value
|
|
220
307
|
|
|
221
308
|
query = """
|
|
222
|
-
MATCH (pool:%(number_pool)s { uuid: $pool_id })
|
|
223
|
-
|
|
224
|
-
|
|
309
|
+
MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValue)
|
|
310
|
+
WHERE toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
|
|
311
|
+
CALL (pool, res, av) {
|
|
312
|
+
MATCH (pool)-[res]->(av)<-[hv:HAS_VALUE]-(attr:Attribute)<-[ha:HAS_ATTRIBUTE]-(n:%(node)s)
|
|
225
313
|
WHERE
|
|
226
|
-
|
|
227
|
-
AND
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
314
|
+
n.uuid = res.identifier AND
|
|
315
|
+
attr.name = $attribute_name AND
|
|
316
|
+
all(r in [res, hv, ha] WHERE (%(branch_filter)s))
|
|
317
|
+
ORDER BY res.branch_level DESC, hv.branch_level DESC, ha.branch_level DESC, res.from DESC, hv.from DESC, ha.from DESC
|
|
318
|
+
RETURN (res.status = "active" AND hv.status = "active" AND ha.status = "active") AS is_active
|
|
319
|
+
LIMIT 1
|
|
232
320
|
}
|
|
233
|
-
WITH av, is_active
|
|
234
|
-
WHERE is_active =
|
|
321
|
+
WITH av, res, is_active
|
|
322
|
+
WHERE is_active = True
|
|
235
323
|
""" % {
|
|
236
324
|
"branch_filter": branch_filter,
|
|
237
325
|
"number_pool": InfrahubKind.NUMBERPOOL,
|
|
326
|
+
"node": self.pool.node.value,
|
|
238
327
|
}
|
|
328
|
+
|
|
239
329
|
self.add_to_query(query)
|
|
240
|
-
self.return_labels = ["av.value"]
|
|
241
|
-
self.order_by = ["
|
|
330
|
+
self.return_labels = ["DISTINCT(av.value) as value", "res.identifier as identifier"]
|
|
331
|
+
self.order_by = ["value"]
|
|
332
|
+
|
|
333
|
+
def iter_results(self) -> Generator[NumberPoolIdentifierData]:
|
|
334
|
+
for result in self.results:
|
|
335
|
+
yield self.return_model.model_construct(
|
|
336
|
+
value=result.get_as_type("value", return_type=int),
|
|
337
|
+
identifier=result.get_as_type("identifier", return_type=str),
|
|
338
|
+
)
|
|
242
339
|
|
|
243
340
|
|
|
244
341
|
class NumberPoolSetReserved(Query):
|
infrahub/core/schema/__init__.py
CHANGED
|
@@ -46,6 +46,7 @@ class SchemaExtension(HashableModel):
|
|
|
46
46
|
|
|
47
47
|
class SchemaRoot(BaseModel):
|
|
48
48
|
model_config = ConfigDict(extra="forbid")
|
|
49
|
+
|
|
49
50
|
version: str | None = Field(default=None)
|
|
50
51
|
generics: list[GenericSchema] = Field(default_factory=list)
|
|
51
52
|
nodes: list[NodeSchema] = Field(default_factory=list)
|
|
@@ -93,6 +94,10 @@ class SchemaRoot(BaseModel):
|
|
|
93
94
|
"""Return a new `SchemaRoot` after merging `self` with `schema`."""
|
|
94
95
|
return SchemaRoot.model_validate(deep_merge_dict(dicta=self.model_dump(), dictb=schema.model_dump()))
|
|
95
96
|
|
|
97
|
+
def duplicate(self) -> SchemaRoot:
|
|
98
|
+
"""Return a duplicate of the current schema."""
|
|
99
|
+
return SchemaRoot.model_validate(self.model_dump())
|
|
100
|
+
|
|
96
101
|
|
|
97
102
|
internal_schema = internal.to_dict()
|
|
98
103
|
|
|
@@ -165,3 +165,9 @@ class NumberPoolParameters(AttributeParameters):
|
|
|
165
165
|
if self.start_range > self.end_range:
|
|
166
166
|
raise ValueError("`start_range` can't be less than `end_range`")
|
|
167
167
|
return self
|
|
168
|
+
|
|
169
|
+
def get_pool_size(self) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Returns the size of the pool based on the defined ranges.
|
|
172
|
+
"""
|
|
173
|
+
return self.end_range - self.start_range + 1
|
|
@@ -10,6 +10,7 @@ from infrahub import config
|
|
|
10
10
|
from infrahub.core.constants.schema import UpdateSupport
|
|
11
11
|
from infrahub.core.enums import generate_python_enum
|
|
12
12
|
from infrahub.core.query.attribute import default_attribute_query_filter
|
|
13
|
+
from infrahub.exceptions import InitializationError
|
|
13
14
|
from infrahub.types import ATTRIBUTE_KIND_LABELS, ATTRIBUTE_TYPES
|
|
14
15
|
|
|
15
16
|
from .attribute_parameters import (
|
|
@@ -67,6 +68,11 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
67
68
|
def is_deprecated(self) -> bool:
|
|
68
69
|
return bool(self.deprecation)
|
|
69
70
|
|
|
71
|
+
def get_id(self) -> str:
|
|
72
|
+
if self.id is None:
|
|
73
|
+
raise InitializationError("The attribute schema has not been saved yet and doesn't have an id")
|
|
74
|
+
return self.id
|
|
75
|
+
|
|
70
76
|
def to_dict(self) -> dict:
|
|
71
77
|
data = self.model_dump(exclude_unset=True, exclude_none=True)
|
|
72
78
|
for field_name, value in data.items():
|
infrahub/core/schema/manager.py
CHANGED
|
@@ -535,7 +535,7 @@ class SchemaManager(NodeManager):
|
|
|
535
535
|
"""Delete the node with its attributes and relationships."""
|
|
536
536
|
branch = await registry.get_branch(branch=branch, db=db)
|
|
537
537
|
|
|
538
|
-
obj = await self.get_one(id=node.get_id(), branch=branch, db=db)
|
|
538
|
+
obj = await self.get_one(id=node.get_id(), branch=branch, db=db, prefetch_relationships=True)
|
|
539
539
|
if not obj:
|
|
540
540
|
raise SchemaNotFoundError(
|
|
541
541
|
branch_name=branch.name,
|
|
@@ -544,16 +544,10 @@ class SchemaManager(NodeManager):
|
|
|
544
544
|
)
|
|
545
545
|
|
|
546
546
|
# First delete the attributes and the relationships
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
include_owner=True,
|
|
552
|
-
include_source=True,
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
for item in items.values():
|
|
556
|
-
await item.delete(db=db)
|
|
547
|
+
for attr_schema_node in (await obj.attributes.get_peers(db=db)).values():
|
|
548
|
+
await attr_schema_node.delete(db=db)
|
|
549
|
+
for rel_schema_node in (await obj.relationships.get_peers(db=db)).values():
|
|
550
|
+
await rel_schema_node.delete(db=db)
|
|
557
551
|
|
|
558
552
|
await obj.delete(db=db)
|
|
559
553
|
|
|
@@ -9,6 +9,7 @@ from infrahub import config
|
|
|
9
9
|
from infrahub.core.constants import RelationshipDirection
|
|
10
10
|
from infrahub.core.query import QueryNode, QueryRel, QueryRelDirection
|
|
11
11
|
from infrahub.core.relationship import Relationship
|
|
12
|
+
from infrahub.exceptions import InitializationError
|
|
12
13
|
|
|
13
14
|
from .generated.relationship_schema import GeneratedRelationshipSchema
|
|
14
15
|
|
|
@@ -57,6 +58,11 @@ class RelationshipSchema(GeneratedRelationshipSchema):
|
|
|
57
58
|
raise ValueError("RelationshipSchema is not initialized")
|
|
58
59
|
return self.identifier
|
|
59
60
|
|
|
61
|
+
def get_id(self) -> str:
|
|
62
|
+
if not self.id:
|
|
63
|
+
raise InitializationError("The relationship schema has not been saved yet and doesn't have an id")
|
|
64
|
+
return self.id
|
|
65
|
+
|
|
60
66
|
def get_query_arrows(self) -> QueryArrows:
|
|
61
67
|
"""Return (in 4 parts) the 2 arrows for the relationship R1 and R2 based on the direction of the relationship."""
|
|
62
68
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import hashlib
|
|
5
|
+
import keyword
|
|
5
6
|
from collections import defaultdict
|
|
6
7
|
from itertools import chain, combinations
|
|
7
8
|
from typing import Any
|
|
@@ -518,6 +519,7 @@ class SchemaBranch:
|
|
|
518
519
|
|
|
519
520
|
def process_validate(self) -> None:
|
|
520
521
|
self.validate_names()
|
|
522
|
+
self.validate_python_keywords()
|
|
521
523
|
self.validate_kinds()
|
|
522
524
|
self.validate_computed_attributes()
|
|
523
525
|
self.validate_attribute_parameters()
|
|
@@ -987,6 +989,26 @@ class SchemaBranch:
|
|
|
987
989
|
):
|
|
988
990
|
raise ValueError(f"{node.kind}: {rel.name} isn't allowed as a relationship name.")
|
|
989
991
|
|
|
992
|
+
def validate_python_keywords(self) -> None:
|
|
993
|
+
"""Validate that attribute and relationship names don't use Python keywords."""
|
|
994
|
+
for name in self.all_names:
|
|
995
|
+
node = self.get(name=name, duplicate=False)
|
|
996
|
+
|
|
997
|
+
# Check for Python keywords in attribute names
|
|
998
|
+
for attribute in node.attributes:
|
|
999
|
+
if keyword.iskeyword(attribute.name):
|
|
1000
|
+
raise ValueError(
|
|
1001
|
+
f"Python keyword '{attribute.name}' cannot be used as an attribute name on '{node.kind}'"
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
# Check for Python keywords in relationship names
|
|
1005
|
+
if config.SETTINGS.main.schema_strict_mode:
|
|
1006
|
+
for relationship in node.relationships:
|
|
1007
|
+
if keyword.iskeyword(relationship.name):
|
|
1008
|
+
raise ValueError(
|
|
1009
|
+
f"Python keyword '{relationship.name}' cannot be used as a relationship name on '{node.kind}' when using strict mode"
|
|
1010
|
+
)
|
|
1011
|
+
|
|
990
1012
|
def _validate_common_parent(self, node: NodeSchema, rel: RelationshipSchema) -> None:
|
|
991
1013
|
if not rel.common_parent:
|
|
992
1014
|
return
|
|
@@ -1588,7 +1610,8 @@ class SchemaBranch:
|
|
|
1588
1610
|
|
|
1589
1611
|
self.set(name=name, schema=node)
|
|
1590
1612
|
|
|
1591
|
-
def
|
|
1613
|
+
def _generate_weight_generics(self) -> None:
|
|
1614
|
+
"""Generate order_weight for all generic schemas."""
|
|
1592
1615
|
for name in self.generic_names:
|
|
1593
1616
|
node = self.get(name=name, duplicate=False)
|
|
1594
1617
|
|
|
@@ -1606,6 +1629,8 @@ class SchemaBranch:
|
|
|
1606
1629
|
|
|
1607
1630
|
self.set(name=name, schema=node)
|
|
1608
1631
|
|
|
1632
|
+
def _generate_weight_nodes_profiles(self) -> None:
|
|
1633
|
+
"""Generate order_weight for all nodes and profiles."""
|
|
1609
1634
|
for name in self.node_names + self.profile_names:
|
|
1610
1635
|
node = self.get(name=name, duplicate=False)
|
|
1611
1636
|
|
|
@@ -1630,6 +1655,33 @@ class SchemaBranch:
|
|
|
1630
1655
|
|
|
1631
1656
|
self.set(name=name, schema=node)
|
|
1632
1657
|
|
|
1658
|
+
def _generate_weight_templates(self) -> None:
|
|
1659
|
+
"""Generate order_weight for all templates.
|
|
1660
|
+
|
|
1661
|
+
The order of the fields for the template must respect the order of the node.
|
|
1662
|
+
"""
|
|
1663
|
+
for name in self.template_names:
|
|
1664
|
+
template = self.get(name=name, duplicate=True)
|
|
1665
|
+
node = self.get(name=template.name, duplicate=False)
|
|
1666
|
+
|
|
1667
|
+
node_weights = {
|
|
1668
|
+
item.name: item.order_weight
|
|
1669
|
+
for item in node.attributes + node.relationships
|
|
1670
|
+
if item.order_weight is not None
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
for item in template.attributes + template.relationships:
|
|
1674
|
+
if item.order_weight:
|
|
1675
|
+
continue
|
|
1676
|
+
item.order_weight = node_weights[item.name] + 10000 if item.name in node_weights else None
|
|
1677
|
+
|
|
1678
|
+
self.set(name=name, schema=template)
|
|
1679
|
+
|
|
1680
|
+
def generate_weight(self) -> None:
|
|
1681
|
+
self._generate_weight_generics()
|
|
1682
|
+
self._generate_weight_nodes_profiles()
|
|
1683
|
+
self._generate_weight_templates()
|
|
1684
|
+
|
|
1633
1685
|
def cleanup_inherited_elements(self) -> None:
|
|
1634
1686
|
for name in self.node_names:
|
|
1635
1687
|
node = self.get_node(name=name, duplicate=False)
|
|
@@ -2038,25 +2090,34 @@ class SchemaBranch:
|
|
|
2038
2090
|
if relationship.kind not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC]
|
|
2039
2091
|
else relationship.peer
|
|
2040
2092
|
)
|
|
2093
|
+
|
|
2094
|
+
is_optional = (
|
|
2095
|
+
relationship.optional if is_autogenerated_subtemplate else relationship.kind != RelationshipKind.PARENT
|
|
2096
|
+
)
|
|
2097
|
+
identifier = (
|
|
2098
|
+
f"template_{relationship.identifier}"
|
|
2099
|
+
if relationship.identifier
|
|
2100
|
+
else self._generate_identifier_string(template_schema.kind, rel_template_peer)
|
|
2101
|
+
)
|
|
2102
|
+
label = (
|
|
2103
|
+
f"{relationship.name} template".title()
|
|
2104
|
+
if relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
|
|
2105
|
+
else relationship.name.title()
|
|
2106
|
+
)
|
|
2107
|
+
|
|
2041
2108
|
template_schema.relationships.append(
|
|
2042
2109
|
RelationshipSchema(
|
|
2043
2110
|
name=relationship.name,
|
|
2044
2111
|
peer=rel_template_peer,
|
|
2045
2112
|
kind=relationship.kind,
|
|
2046
|
-
optional=
|
|
2047
|
-
if is_autogenerated_subtemplate
|
|
2048
|
-
else relationship.kind != RelationshipKind.PARENT,
|
|
2113
|
+
optional=is_optional,
|
|
2049
2114
|
cardinality=relationship.cardinality,
|
|
2050
2115
|
direction=relationship.direction,
|
|
2051
2116
|
branch=relationship.branch,
|
|
2052
|
-
identifier=
|
|
2053
|
-
if relationship.identifier
|
|
2054
|
-
else self._generate_identifier_string(template_schema.kind, rel_template_peer),
|
|
2117
|
+
identifier=identifier,
|
|
2055
2118
|
min_count=relationship.min_count,
|
|
2056
2119
|
max_count=relationship.max_count,
|
|
2057
|
-
label=
|
|
2058
|
-
if relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
|
|
2059
|
-
else relationship.name.title(),
|
|
2120
|
+
label=label,
|
|
2060
2121
|
inherited=relationship.inherited,
|
|
2061
2122
|
)
|
|
2062
2123
|
)
|
|
@@ -2144,7 +2205,7 @@ class SchemaBranch:
|
|
|
2144
2205
|
attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
|
|
2145
2206
|
attr = attr_schema_class(
|
|
2146
2207
|
optional=node_attr.optional if is_autogenerated_subtemplate else True,
|
|
2147
|
-
**node_attr.model_dump(exclude=["id", "unique", "optional", "read_only"]),
|
|
2208
|
+
**node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "order_weight"]),
|
|
2148
2209
|
)
|
|
2149
2210
|
template.attributes.append(attr)
|
|
2150
2211
|
|
|
@@ -2,6 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from infrahub.core import registry
|
|
6
|
+
from infrahub.core.schema.attribute_parameters import NumberPoolParameters
|
|
7
|
+
|
|
5
8
|
from ..interface import ConstraintCheckerInterface
|
|
6
9
|
from ..query import NodeNotPresentValidatorQuery
|
|
7
10
|
|
|
@@ -31,10 +34,22 @@ class NodeAttributeAddChecker(ConstraintCheckerInterface):
|
|
|
31
34
|
grouped_data_paths_list: list[GroupedDataPaths] = []
|
|
32
35
|
if not request.schema_path.field_name:
|
|
33
36
|
raise ValueError("field_name is not defined")
|
|
37
|
+
|
|
34
38
|
attribute_schema = request.node_schema.get_attribute(name=request.schema_path.field_name)
|
|
35
39
|
if attribute_schema.optional is True or attribute_schema.default_value is not None:
|
|
36
40
|
return grouped_data_paths_list
|
|
37
41
|
|
|
42
|
+
# If the attribute is a NumberPool, we need to ensure that the pool is big enough for all existing nodes
|
|
43
|
+
if attribute_schema.kind == "NumberPool" and isinstance(attribute_schema.parameters, NumberPoolParameters):
|
|
44
|
+
nbr_nodes = await registry.manager.count(db=self.db, branch=self.branch, schema=request.node_schema)
|
|
45
|
+
pool_size = attribute_schema.parameters.get_pool_size()
|
|
46
|
+
|
|
47
|
+
if pool_size < nbr_nodes:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"The size of the NumberPool is smaller than the number of existing nodes {pool_size} < {nbr_nodes}."
|
|
50
|
+
)
|
|
51
|
+
return grouped_data_paths_list
|
|
52
|
+
|
|
38
53
|
for query_class in self.query_classes:
|
|
39
54
|
# TODO add exception handling
|
|
40
55
|
query = await query_class.init(
|
|
@@ -11,9 +11,7 @@ from infrahub.core.branch import Branch # noqa: TC001
|
|
|
11
11
|
from infrahub.core.path import SchemaPath # noqa: TC001
|
|
12
12
|
from infrahub.core.schema import GenericSchema, NodeSchema
|
|
13
13
|
from infrahub.core.validators.aggregated_checker import AggregatedConstraintChecker
|
|
14
|
-
from infrahub.core.validators.model import
|
|
15
|
-
SchemaConstraintValidatorRequest,
|
|
16
|
-
)
|
|
14
|
+
from infrahub.core.validators.model import SchemaConstraintValidatorRequest, SchemaViolation
|
|
17
15
|
from infrahub.dependencies.registry import get_component_registry
|
|
18
16
|
from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
|
|
19
17
|
from infrahub.workflows.utils import add_tags
|
|
@@ -84,7 +82,17 @@ async def schema_path_validate(
|
|
|
84
82
|
aggregated_constraint_checker = await component_registry.get_component(
|
|
85
83
|
AggregatedConstraintChecker, db=db, branch=branch
|
|
86
84
|
)
|
|
87
|
-
|
|
85
|
+
try:
|
|
86
|
+
violations = await aggregated_constraint_checker.run_constraints(constraint_request)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
violation = SchemaViolation(
|
|
89
|
+
node_id="unknown",
|
|
90
|
+
node_kind=node_schema.kind,
|
|
91
|
+
display_label=f"Error validating {constraint_name} on {node_schema.kind}",
|
|
92
|
+
full_display_label=f"Error validating {constraint_name} on {node_schema.kind}",
|
|
93
|
+
message=str(exc),
|
|
94
|
+
)
|
|
95
|
+
violations = [violation]
|
|
88
96
|
|
|
89
97
|
return SchemaValidatorPathResponseData(
|
|
90
98
|
violations=violations, constraint_name=constraint_name, schema_path=schema_path
|
infrahub/generators/tasks.py
CHANGED
|
@@ -214,7 +214,7 @@ async def request_generator_definition_run(
|
|
|
214
214
|
repository_kind=repository.typename,
|
|
215
215
|
branch_name=model.branch,
|
|
216
216
|
query=model.generator_definition.query_name,
|
|
217
|
-
variables=member.extract(params=model.generator_definition.parameters),
|
|
217
|
+
variables=await member.extract(params=model.generator_definition.parameters),
|
|
218
218
|
target_id=member.id,
|
|
219
219
|
target_name=member.display_label,
|
|
220
220
|
)
|
infrahub/git/integrator.py
CHANGED
|
@@ -1294,7 +1294,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
|
|
|
1294
1294
|
query: CoreGraphQLQuery,
|
|
1295
1295
|
) -> ArtifactGenerateResult:
|
|
1296
1296
|
"""It doesn't look like this is used anywhere today ... we should either remove it or refactor render_artifact below to use this."""
|
|
1297
|
-
variables = target.extract(params=definition.parameters.value)
|
|
1297
|
+
variables = await target.extract(params=definition.parameters.value)
|
|
1298
1298
|
response = await self.sdk.query_gql_query(
|
|
1299
1299
|
name=query.name.value,
|
|
1300
1300
|
variables=variables,
|
infrahub/git/tasks.py
CHANGED
|
@@ -365,7 +365,7 @@ async def generate_request_artifact_definition(
|
|
|
365
365
|
repository_kind=repository.get_kind(),
|
|
366
366
|
branch_name=model.branch,
|
|
367
367
|
query=query.name.value,
|
|
368
|
-
variables=member.extract(params=artifact_definition.parameters.value),
|
|
368
|
+
variables=await member.extract(params=artifact_definition.parameters.value),
|
|
369
369
|
target_id=member.id,
|
|
370
370
|
target_name=member.display_label,
|
|
371
371
|
target_kind=member.get_kind(),
|
|
@@ -583,7 +583,7 @@ async def trigger_repository_user_checks_definitions(
|
|
|
583
583
|
branch_name=model.branch_name,
|
|
584
584
|
check_definition_id=model.check_definition_id,
|
|
585
585
|
proposed_change=model.proposed_change,
|
|
586
|
-
variables=member.extract(params=definition.parameters.value),
|
|
586
|
+
variables=await member.extract(params=definition.parameters.value),
|
|
587
587
|
branch_diff=model.branch_diff,
|
|
588
588
|
timeout=definition.timeout.value,
|
|
589
589
|
)
|