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.
Files changed (45) hide show
  1. infrahub/api/schema.py +2 -2
  2. infrahub/cli/db.py +34 -0
  3. infrahub/core/convert_object_type/conversion.py +10 -0
  4. infrahub/core/diff/enricher/hierarchy.py +7 -3
  5. infrahub/core/diff/query_parser.py +7 -3
  6. infrahub/core/graph/__init__.py +1 -1
  7. infrahub/core/migrations/graph/__init__.py +2 -0
  8. infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +84 -0
  9. infrahub/core/migrations/query/node_duplicate.py +5 -1
  10. infrahub/core/migrations/schema/node_attribute_add.py +55 -2
  11. infrahub/core/migrations/shared.py +37 -9
  12. infrahub/core/node/__init__.py +41 -21
  13. infrahub/core/node/resource_manager/number_pool.py +60 -22
  14. infrahub/core/query/diff.py +17 -3
  15. infrahub/core/query/relationship.py +8 -11
  16. infrahub/core/query/resource_manager.py +117 -20
  17. infrahub/core/schema/__init__.py +5 -0
  18. infrahub/core/schema/attribute_parameters.py +6 -0
  19. infrahub/core/schema/attribute_schema.py +6 -0
  20. infrahub/core/schema/manager.py +5 -11
  21. infrahub/core/schema/relationship_schema.py +6 -0
  22. infrahub/core/schema/schema_branch.py +72 -11
  23. infrahub/core/validators/node/attribute.py +15 -0
  24. infrahub/core/validators/tasks.py +12 -4
  25. infrahub/generators/tasks.py +1 -1
  26. infrahub/git/integrator.py +1 -1
  27. infrahub/git/tasks.py +2 -2
  28. infrahub/graphql/mutations/main.py +24 -5
  29. infrahub/graphql/queries/resource_manager.py +4 -4
  30. infrahub/proposed_change/tasks.py +2 -2
  31. infrahub/tasks/registry.py +63 -35
  32. infrahub_sdk/client.py +7 -8
  33. infrahub_sdk/ctl/utils.py +3 -0
  34. infrahub_sdk/node/node.py +87 -15
  35. infrahub_sdk/node/relationship.py +43 -2
  36. infrahub_sdk/protocols_base.py +0 -2
  37. infrahub_sdk/protocols_generator/constants.py +1 -0
  38. infrahub_sdk/utils.py +0 -17
  39. infrahub_sdk/yaml.py +13 -7
  40. infrahub_server-1.3.5.dist-info/LICENSE.txt +201 -0
  41. {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/METADATA +3 -3
  42. {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/RECORD +44 -43
  43. infrahub_server-1.3.3.dist-info/LICENSE.txt +0 -661
  44. {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/WHEEL +0 -0
  45. {infrahub_server-1.3.3.dist-info → infrahub_server-1.3.5.dist-info}/entry_points.txt +0 -0
@@ -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
- ORDER BY base_r_prop.from DESC, base_r_node.from DESC, base_r_root.from DESC
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 relationships(path) WHERE (%(branch_filter)s))
663
- WITH source_node, peer, rl, relationships(path) as rels, %(branch_level)s AS branch_level, %(froms)s AS froms
664
- RETURN peer as peer, rels, rl as rl1
665
- ORDER BY branch_level DESC, froms[-1] DESC, froms[-2] DESC
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, rl1 as rl, rels, source_node
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 = ['all(r IN rels WHERE r.status = "active")']
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
- r.identifier = $identifier
183
- AND
197
+ %(identifier_filter)s
184
198
  %(branch_filter)s
185
- """ % {"branch_filter": branch_filter, "number_pool": InfrahubKind.NUMBERPOOL}
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("reservation.value", return_type=int)
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
- CALL (pool) {
224
- MATCH (pool)-[res:IS_RESERVED]->(av:AttributeValue)<-[hv:HAS_VALUE]-(attr:Attribute)
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
- attr.name = $attribute_name
227
- AND
228
- toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
229
- AND
230
- all(r in [res, hv] WHERE (%(branch_filter)s))
231
- RETURN av, (res.status = "active" AND hv.status = "active") AS is_active
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 = TRUE
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 = ["av.value"]
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):
@@ -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():
@@ -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
- items = await self.get_many(
548
- ids=[item.id for item in node.local_attributes + node.local_relationships if item.id],
549
- db=db,
550
- branch=branch,
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 generate_weight(self) -> None:
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=relationship.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=f"template_{relationship.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=f"{relationship.name} template".title()
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
- violations = await aggregated_constraint_checker.run_constraints(constraint_request)
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
@@ -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
  )
@@ -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
  )