infrahub-server 1.4.12__py3-none-any.whl → 1.5.0__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 (234) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/internal.py +2 -0
  5. infrahub/api/query.py +2 -0
  6. infrahub/api/schema.py +27 -3
  7. infrahub/auth.py +5 -5
  8. infrahub/cli/__init__.py +2 -0
  9. infrahub/cli/db.py +160 -157
  10. infrahub/cli/dev.py +118 -0
  11. infrahub/cli/tasks.py +46 -0
  12. infrahub/cli/upgrade.py +56 -9
  13. infrahub/computed_attribute/tasks.py +19 -7
  14. infrahub/config.py +7 -2
  15. infrahub/core/attribute.py +35 -24
  16. infrahub/core/branch/enums.py +1 -1
  17. infrahub/core/branch/models.py +9 -5
  18. infrahub/core/branch/needs_rebase_status.py +11 -0
  19. infrahub/core/branch/tasks.py +72 -10
  20. infrahub/core/changelog/models.py +2 -10
  21. infrahub/core/constants/__init__.py +4 -0
  22. infrahub/core/constants/infrahubkind.py +1 -0
  23. infrahub/core/convert_object_type/object_conversion.py +201 -0
  24. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  25. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  26. infrahub/core/diff/calculator.py +2 -2
  27. infrahub/core/diff/model/path.py +4 -0
  28. infrahub/core/diff/payload_builder.py +1 -1
  29. infrahub/core/diff/query/artifact.py +1 -0
  30. infrahub/core/diff/query/delete_query.py +9 -5
  31. infrahub/core/diff/query/field_summary.py +1 -0
  32. infrahub/core/diff/query/merge.py +39 -23
  33. infrahub/core/graph/__init__.py +1 -1
  34. infrahub/core/initialization.py +7 -4
  35. infrahub/core/manager.py +3 -81
  36. infrahub/core/migrations/__init__.py +3 -0
  37. infrahub/core/migrations/exceptions.py +4 -0
  38. infrahub/core/migrations/graph/__init__.py +13 -10
  39. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  40. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  41. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  42. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  43. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +149 -0
  44. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  45. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  46. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  47. infrahub/core/migrations/query/__init__.py +7 -8
  48. infrahub/core/migrations/query/attribute_add.py +8 -6
  49. infrahub/core/migrations/query/attribute_remove.py +134 -0
  50. infrahub/core/migrations/runner.py +54 -0
  51. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  52. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  53. infrahub/core/migrations/schema/node_attribute_add.py +26 -5
  54. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  55. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  56. infrahub/core/migrations/schema/node_remove.py +2 -1
  57. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  58. infrahub/core/migrations/shared.py +66 -19
  59. infrahub/core/models.py +2 -2
  60. infrahub/core/node/__init__.py +207 -54
  61. infrahub/core/node/create.py +53 -49
  62. infrahub/core/node/lock_utils.py +124 -0
  63. infrahub/core/node/node_property_attribute.py +230 -0
  64. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  65. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  66. infrahub/core/node/resource_manager/number_pool.py +2 -1
  67. infrahub/core/node/standard.py +1 -1
  68. infrahub/core/property.py +11 -0
  69. infrahub/core/protocols.py +8 -1
  70. infrahub/core/query/attribute.py +82 -15
  71. infrahub/core/query/diff.py +61 -16
  72. infrahub/core/query/ipam.py +16 -4
  73. infrahub/core/query/node.py +92 -212
  74. infrahub/core/query/relationship.py +44 -26
  75. infrahub/core/query/subquery.py +0 -8
  76. infrahub/core/relationship/model.py +69 -24
  77. infrahub/core/schema/__init__.py +56 -0
  78. infrahub/core/schema/attribute_schema.py +4 -2
  79. infrahub/core/schema/basenode_schema.py +42 -2
  80. infrahub/core/schema/definitions/core/__init__.py +2 -0
  81. infrahub/core/schema/definitions/core/check.py +1 -1
  82. infrahub/core/schema/definitions/core/generator.py +2 -0
  83. infrahub/core/schema/definitions/core/group.py +16 -2
  84. infrahub/core/schema/definitions/core/repository.py +7 -0
  85. infrahub/core/schema/definitions/core/transform.py +1 -1
  86. infrahub/core/schema/definitions/internal.py +12 -3
  87. infrahub/core/schema/generated/attribute_schema.py +2 -2
  88. infrahub/core/schema/generated/base_node_schema.py +6 -1
  89. infrahub/core/schema/manager.py +3 -0
  90. infrahub/core/schema/node_schema.py +1 -0
  91. infrahub/core/schema/relationship_schema.py +0 -1
  92. infrahub/core/schema/schema_branch.py +295 -10
  93. infrahub/core/schema/schema_branch_display.py +135 -0
  94. infrahub/core/schema/schema_branch_hfid.py +120 -0
  95. infrahub/core/validators/aggregated_checker.py +1 -1
  96. infrahub/database/graph.py +21 -0
  97. infrahub/display_labels/__init__.py +0 -0
  98. infrahub/display_labels/gather.py +48 -0
  99. infrahub/display_labels/models.py +240 -0
  100. infrahub/display_labels/tasks.py +192 -0
  101. infrahub/display_labels/triggers.py +22 -0
  102. infrahub/events/branch_action.py +27 -1
  103. infrahub/events/group_action.py +1 -1
  104. infrahub/events/node_action.py +1 -1
  105. infrahub/generators/constants.py +7 -0
  106. infrahub/generators/models.py +38 -12
  107. infrahub/generators/tasks.py +34 -16
  108. infrahub/git/base.py +42 -2
  109. infrahub/git/integrator.py +22 -14
  110. infrahub/git/tasks.py +52 -2
  111. infrahub/graphql/analyzer.py +9 -0
  112. infrahub/graphql/api/dependencies.py +2 -4
  113. infrahub/graphql/api/endpoints.py +16 -6
  114. infrahub/graphql/app.py +2 -4
  115. infrahub/graphql/initialization.py +2 -3
  116. infrahub/graphql/manager.py +213 -137
  117. infrahub/graphql/middleware.py +12 -0
  118. infrahub/graphql/mutations/branch.py +16 -0
  119. infrahub/graphql/mutations/computed_attribute.py +110 -3
  120. infrahub/graphql/mutations/convert_object_type.py +44 -13
  121. infrahub/graphql/mutations/display_label.py +118 -0
  122. infrahub/graphql/mutations/generator.py +25 -7
  123. infrahub/graphql/mutations/hfid.py +125 -0
  124. infrahub/graphql/mutations/ipam.py +73 -41
  125. infrahub/graphql/mutations/main.py +61 -178
  126. infrahub/graphql/mutations/profile.py +195 -0
  127. infrahub/graphql/mutations/proposed_change.py +8 -1
  128. infrahub/graphql/mutations/relationship.py +2 -2
  129. infrahub/graphql/mutations/repository.py +22 -83
  130. infrahub/graphql/mutations/resource_manager.py +2 -2
  131. infrahub/graphql/mutations/webhook.py +1 -1
  132. infrahub/graphql/queries/resource_manager.py +1 -1
  133. infrahub/graphql/registry.py +173 -0
  134. infrahub/graphql/resolvers/resolver.py +2 -0
  135. infrahub/graphql/schema.py +8 -1
  136. infrahub/graphql/schema_sort.py +170 -0
  137. infrahub/graphql/types/branch.py +4 -1
  138. infrahub/graphql/types/enums.py +3 -0
  139. infrahub/groups/tasks.py +1 -1
  140. infrahub/hfid/__init__.py +0 -0
  141. infrahub/hfid/gather.py +48 -0
  142. infrahub/hfid/models.py +240 -0
  143. infrahub/hfid/tasks.py +191 -0
  144. infrahub/hfid/triggers.py +22 -0
  145. infrahub/lock.py +119 -42
  146. infrahub/locks/__init__.py +0 -0
  147. infrahub/locks/tasks.py +37 -0
  148. infrahub/message_bus/types.py +1 -0
  149. infrahub/patch/plan_writer.py +2 -2
  150. infrahub/permissions/constants.py +2 -0
  151. infrahub/profiles/__init__.py +0 -0
  152. infrahub/profiles/node_applier.py +101 -0
  153. infrahub/profiles/queries/__init__.py +0 -0
  154. infrahub/profiles/queries/get_profile_data.py +98 -0
  155. infrahub/profiles/tasks.py +63 -0
  156. infrahub/proposed_change/tasks.py +67 -14
  157. infrahub/repositories/__init__.py +0 -0
  158. infrahub/repositories/create_repository.py +113 -0
  159. infrahub/server.py +9 -1
  160. infrahub/services/__init__.py +8 -5
  161. infrahub/services/adapters/http/__init__.py +5 -0
  162. infrahub/services/adapters/workflow/worker.py +14 -3
  163. infrahub/task_manager/event.py +5 -0
  164. infrahub/task_manager/models.py +7 -0
  165. infrahub/task_manager/task.py +73 -0
  166. infrahub/tasks/registry.py +6 -4
  167. infrahub/trigger/catalogue.py +4 -0
  168. infrahub/trigger/models.py +2 -0
  169. infrahub/trigger/setup.py +13 -4
  170. infrahub/trigger/tasks.py +6 -0
  171. infrahub/webhook/models.py +1 -1
  172. infrahub/workers/dependencies.py +3 -1
  173. infrahub/workers/infrahub_async.py +10 -2
  174. infrahub/workflows/catalogue.py +118 -3
  175. infrahub/workflows/initialization.py +21 -0
  176. infrahub/workflows/models.py +17 -2
  177. infrahub/workflows/utils.py +2 -1
  178. infrahub_sdk/branch.py +17 -8
  179. infrahub_sdk/checks.py +1 -1
  180. infrahub_sdk/client.py +376 -95
  181. infrahub_sdk/config.py +29 -2
  182. infrahub_sdk/convert_object_type.py +61 -0
  183. infrahub_sdk/ctl/branch.py +3 -0
  184. infrahub_sdk/ctl/check.py +2 -3
  185. infrahub_sdk/ctl/cli_commands.py +20 -12
  186. infrahub_sdk/ctl/config.py +8 -2
  187. infrahub_sdk/ctl/generator.py +6 -3
  188. infrahub_sdk/ctl/graphql.py +184 -0
  189. infrahub_sdk/ctl/repository.py +39 -1
  190. infrahub_sdk/ctl/schema.py +40 -10
  191. infrahub_sdk/ctl/task.py +110 -0
  192. infrahub_sdk/ctl/utils.py +4 -0
  193. infrahub_sdk/ctl/validate.py +5 -3
  194. infrahub_sdk/diff.py +4 -5
  195. infrahub_sdk/exceptions.py +2 -0
  196. infrahub_sdk/generator.py +7 -1
  197. infrahub_sdk/graphql/__init__.py +12 -0
  198. infrahub_sdk/graphql/constants.py +1 -0
  199. infrahub_sdk/graphql/plugin.py +85 -0
  200. infrahub_sdk/graphql/query.py +77 -0
  201. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  202. infrahub_sdk/graphql/utils.py +40 -0
  203. infrahub_sdk/node/attribute.py +2 -0
  204. infrahub_sdk/node/node.py +28 -20
  205. infrahub_sdk/node/relationship.py +1 -3
  206. infrahub_sdk/playback.py +1 -2
  207. infrahub_sdk/protocols.py +54 -6
  208. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  209. infrahub_sdk/pytest_plugin/utils.py +40 -0
  210. infrahub_sdk/repository.py +1 -2
  211. infrahub_sdk/schema/__init__.py +70 -4
  212. infrahub_sdk/schema/main.py +1 -0
  213. infrahub_sdk/schema/repository.py +8 -0
  214. infrahub_sdk/spec/models.py +7 -0
  215. infrahub_sdk/spec/object.py +54 -6
  216. infrahub_sdk/spec/processors/__init__.py +0 -0
  217. infrahub_sdk/spec/processors/data_processor.py +10 -0
  218. infrahub_sdk/spec/processors/factory.py +34 -0
  219. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  220. infrahub_sdk/spec/range_expansion.py +118 -0
  221. infrahub_sdk/task/models.py +6 -4
  222. infrahub_sdk/timestamp.py +18 -6
  223. infrahub_sdk/transforms.py +1 -1
  224. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
  225. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +233 -176
  226. infrahub_testcontainers/container.py +114 -2
  227. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  228. infrahub_testcontainers/docker-compose.test.yml +5 -0
  229. infrahub_testcontainers/models.py +2 -2
  230. infrahub_testcontainers/performance_test.py +4 -4
  231. infrahub/core/convert_object_type/conversion.py +0 -134
  232. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
  233. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
  234. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -381,14 +381,33 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
381
381
 
382
382
  node = await self.get_node(db=db)
383
383
 
384
+ flag_properties_to_update = {}
385
+ for prop_name in self._flag_properties:
386
+ if prop_name not in properties_to_update:
387
+ continue
388
+ value = getattr(self, prop_name)
389
+ if value is not None:
390
+ flag_properties_to_update[prop_name] = value
391
+
392
+ node_properties_to_update = {}
393
+ for prop_name in self._node_properties:
394
+ if prop_name not in properties_to_update:
395
+ continue
396
+ if value := getattr(self, f"{prop_name}_id"):
397
+ node_properties_to_update[prop_name] = value
398
+
399
+ if not flag_properties_to_update and not node_properties_to_update:
400
+ return
401
+
384
402
  query = await RelationshipUpdatePropertyQuery.init(
385
403
  db=db,
404
+ branch=branch,
386
405
  source=node,
387
406
  rel=self,
388
- properties_to_update=properties_to_update,
389
- data=data,
390
- branch=branch,
391
407
  at=update_at,
408
+ flag_properties_to_update=flag_properties_to_update,
409
+ node_properties_to_update=node_properties_to_update,
410
+ rel_node_id=data.rel_node_id,
392
411
  )
393
412
  await query.execute(db=db)
394
413
 
@@ -426,15 +445,20 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
426
445
  )
427
446
  await delete_query.execute(db=db)
428
447
 
429
- async def resolve(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
448
+ async def resolve(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> None:
430
449
  """Resolve the peer of the relationship."""
431
450
 
451
+ fields = fields or []
452
+ query_fields = dict.fromkeys(fields)
453
+ if "display_label" not in query_fields:
454
+ query_fields["display_label"] = None
455
+
432
456
  if self._peer is not None:
433
457
  return
434
458
 
435
459
  if self.peer_id and not is_valid_uuid(self.peer_id):
436
460
  peer = await registry.manager.get_one_by_default_filter(
437
- db=db, id=self.peer_id, branch=self.branch, kind=self.schema.peer, fields={"display_label": None}
461
+ db=db, id=self.peer_id, branch=self.branch, kind=self.schema.peer, fields=query_fields
438
462
  )
439
463
  if peer:
440
464
  self.set_peer(value=peer)
@@ -451,7 +475,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
451
475
  hfid=self.peer_hfid,
452
476
  branch=self.branch,
453
477
  kind=kind,
454
- fields={"display_label": None},
478
+ fields=query_fields,
455
479
  raise_on_error=True,
456
480
  )
457
481
  self.set_peer(value=peer)
@@ -570,7 +594,9 @@ class RelationshipValidatorList:
570
594
  ValidationError: If the number of relationships is not within the min and max count.
571
595
  """
572
596
 
573
- def __init__(self, *relationships: Relationship, name: str, min_count: int = 0, max_count: int = 0) -> None:
597
+ def __init__(
598
+ self, *relationships: Relationship, name: str, min_count: int | None = 0, max_count: int | None = 0
599
+ ) -> None:
574
600
  """Initialize list for Relationship but with validation against min/max count.
575
601
 
576
602
  Args:
@@ -580,8 +606,14 @@ class RelationshipValidatorList:
580
606
  Raises:
581
607
  ValidationError: The number of relationships is not within the min and max count.
582
608
  """
583
- if max_count < min_count:
609
+ if max_count is not None and min_count is not None and max_count < min_count:
584
610
  raise ValidationError({"msg": "max_count must be greater than min_count"})
611
+
612
+ if max_count is None:
613
+ max_count = 0
614
+ if min_count is None:
615
+ min_count = 0
616
+
585
617
  self.min_count: int = min_count
586
618
  self.max_count: int = max_count
587
619
  self.name = name
@@ -726,15 +758,22 @@ class RelationshipManager:
726
758
  # TODO Ideally this information should come from the Schema
727
759
  self.rel_class = Relationship
728
760
 
729
- self._relationships: RelationshipValidatorList = RelationshipValidatorList(
730
- name=self.schema.name,
731
- min_count=0 if self.schema.optional else self.schema.min_count,
732
- max_count=self.schema.max_count,
733
- )
761
+ self._relationships: RelationshipValidatorList = self._get_init_relationships()
734
762
  self._relationship_id_details: RelationshipUpdateDetails | None = None
735
763
  self.has_fetched_relationships: bool = False
736
764
  self.lock = asyncio.Lock()
737
765
 
766
+ def _get_init_relationships(self) -> RelationshipValidatorList:
767
+ min_count = self.schema.min_count
768
+ max_count: int | None = self.schema.max_count if self.schema.max_count > 0 else None
769
+ if self.schema.optional:
770
+ min_count = 0
771
+ return RelationshipValidatorList(
772
+ name=self.schema.name,
773
+ min_count=min_count,
774
+ max_count=max_count,
775
+ )
776
+
738
777
  @classmethod
739
778
  async def init(
740
779
  cls,
@@ -909,6 +948,19 @@ class RelationshipManager:
909
948
  return registry.get_global_branch()
910
949
  return self.branch
911
950
 
951
+ async def get_db_peers(
952
+ self, db: InfrahubDatabase, at: Timestamp | None = None, branch_agnostic: bool = False
953
+ ) -> list[RelationshipPeerData]:
954
+ query = await RelationshipGetPeerQuery.init(
955
+ db=db,
956
+ source=self.node,
957
+ at=at or self.at,
958
+ rel=self.rel_class(schema=self.schema, branch=self.branch, node=self.node),
959
+ branch_agnostic=branch_agnostic,
960
+ )
961
+ await query.execute(db=db)
962
+ return list(query.get_peers())
963
+
912
964
  async def fetch_relationship_ids(
913
965
  self,
914
966
  db: InfrahubDatabase,
@@ -926,16 +978,9 @@ class RelationshipManager:
926
978
 
927
979
  current_peer_ids = [rel.get_peer_id() for rel in self._relationships]
928
980
 
929
- query = await RelationshipGetPeerQuery.init(
930
- db=db,
931
- source=self.node,
932
- at=at or self.at,
933
- rel=self.rel_class(schema=self.schema, branch=self.branch, node=self.node),
934
- branch_agnostic=branch_agnostic,
935
- )
936
- await query.execute(db=db)
981
+ peers = await self.get_db_peers(db=db, at=at, branch_agnostic=branch_agnostic)
937
982
 
938
- peers_database: dict = {str(peer.peer_id): peer for peer in query.get_peers()}
983
+ peers_database: dict = {str(peer.peer_id): peer for peer in peers}
939
984
  peer_ids = list(peers_database.keys())
940
985
 
941
986
  # Calculate which peer should be added or removed
@@ -1102,9 +1147,9 @@ class RelationshipManager:
1102
1147
 
1103
1148
  return True
1104
1149
 
1105
- async def resolve(self, db: InfrahubDatabase) -> None:
1150
+ async def resolve(self, db: InfrahubDatabase, fields: list[str] | None = None) -> None:
1106
1151
  for rel in self._relationships:
1107
- await rel.resolve(db=db)
1152
+ await rel.resolve(db=db, fields=fields)
1108
1153
 
1109
1154
  async def remove_locally(
1110
1155
  self,
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import uuid
4
+ from enum import Enum
4
5
  from typing import Any, TypeAlias
5
6
 
6
7
  from infrahub_sdk.utils import deep_merge_dict
@@ -44,6 +45,21 @@ class SchemaExtension(HashableModel):
44
45
  nodes: list[NodeExtensionSchema] = Field(default_factory=list)
45
46
 
46
47
 
48
+ class SchemaWarningType(Enum):
49
+ DEPRECATION = "deprecation"
50
+
51
+
52
+ class SchemaWarningKind(BaseModel):
53
+ kind: str = Field(..., description="The kind impacted by the warning")
54
+ field: str | None = Field(default=None, description="The attribute or relationship impacted by the warning")
55
+
56
+
57
+ class SchemaWarning(BaseModel):
58
+ type: SchemaWarningType = Field(..., description="The type of warning")
59
+ kinds: list[SchemaWarningKind] = Field(default_factory=list, description="The kinds impacted by the warning")
60
+ message: str = Field(..., description="The message that describes the warning")
61
+
62
+
47
63
  class SchemaRoot(BaseModel):
48
64
  model_config = ConfigDict(extra="forbid")
49
65
 
@@ -80,6 +96,46 @@ class SchemaRoot(BaseModel):
80
96
 
81
97
  return errors
82
98
 
99
+ def gather_warnings(self) -> list[SchemaWarning]:
100
+ models = self.nodes + self.generics
101
+ warnings: list[SchemaWarning] = []
102
+ for model in models:
103
+ if model.display_labels is not None:
104
+ warnings.append(
105
+ SchemaWarning(
106
+ type=SchemaWarningType.DEPRECATION,
107
+ kinds=[SchemaWarningKind(kind=model.kind)],
108
+ message="display_labels are deprecated, use display_label instead",
109
+ )
110
+ )
111
+ if model.default_filter is not None:
112
+ warnings.append(
113
+ SchemaWarning(
114
+ type=SchemaWarningType.DEPRECATION,
115
+ kinds=[SchemaWarningKind(kind=model.kind)],
116
+ message="default_filter is deprecated",
117
+ )
118
+ )
119
+ for attribute in model.attributes:
120
+ if attribute.max_length is not None:
121
+ warnings.append(
122
+ SchemaWarning(
123
+ type=SchemaWarningType.DEPRECATION,
124
+ kinds=[SchemaWarningKind(kind=model.kind, field=attribute.name)],
125
+ message="Use of 'max_length' on attributes is deprecated, use parameters instead",
126
+ )
127
+ )
128
+ if attribute.min_length is not None:
129
+ warnings.append(
130
+ SchemaWarning(
131
+ type=SchemaWarningType.DEPRECATION,
132
+ kinds=[SchemaWarningKind(kind=model.kind, field=attribute.name)],
133
+ message="Use of 'min_length' on attributes is deprecated, use parameters instead",
134
+ )
135
+ )
136
+
137
+ return warnings
138
+
83
139
  def generate_uuid(self) -> None:
84
140
  """Generate UUID for all nodes, attributes & relationships
85
141
  Mainly useful during unit tests."""
@@ -68,6 +68,10 @@ class AttributeSchema(GeneratedAttributeSchema):
68
68
  def is_deprecated(self) -> bool:
69
69
  return bool(self.deprecation)
70
70
 
71
+ @property
72
+ def support_profiles(self) -> bool:
73
+ return self.read_only is False and self.optional is True
74
+
71
75
  def get_id(self) -> str:
72
76
  if self.id is None:
73
77
  raise InitializationError("The attribute schema has not been saved yet and doesn't have an id")
@@ -202,7 +206,6 @@ class AttributeSchema(GeneratedAttributeSchema):
202
206
  param_prefix: str | None = None,
203
207
  db: InfrahubDatabase | None = None,
204
208
  partial_match: bool = False,
205
- support_profiles: bool = False,
206
209
  ) -> tuple[list[QueryElement], dict[str, Any], list[str]]:
207
210
  if self.enum:
208
211
  filter_value = self.convert_enum_to_value(filter_value)
@@ -217,7 +220,6 @@ class AttributeSchema(GeneratedAttributeSchema):
217
220
  param_prefix=param_prefix,
218
221
  db=db,
219
222
  partial_match=partial_match,
220
- support_profiles=support_profiles,
221
223
  )
222
224
 
223
225
 
@@ -9,7 +9,7 @@ from enum import Enum
9
9
  from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, overload
10
10
 
11
11
  from infrahub_sdk.utils import compare_lists, intersection
12
- from pydantic import field_validator
12
+ from pydantic import ConfigDict, field_validator
13
13
 
14
14
  from infrahub.core.constants import HashableModelState, RelationshipCardinality, RelationshipKind
15
15
  from infrahub.core.models import HashableModel, HashableModelDiff
@@ -19,6 +19,7 @@ from .generated.base_node_schema import GeneratedBaseNodeSchema
19
19
  from .relationship_schema import RelationshipSchema
20
20
 
21
21
  if TYPE_CHECKING:
22
+ from pydantic.config import JsonDict
22
23
  from typing_extensions import Self
23
24
 
24
25
  from infrahub.core.schema import GenericSchema, NodeSchema
@@ -26,6 +27,7 @@ if TYPE_CHECKING:
26
27
 
27
28
 
28
29
  NODE_METADATA_ATTRIBUTES = ["_source", "_owner"]
30
+ NODE_PROPERTY_ATTRIBUTES = ["display_label", "human_friendly_id"]
29
31
  INHERITED = "INHERITED"
30
32
 
31
33
  OPTIONAL_TEXT_FIELDS = [
@@ -39,10 +41,43 @@ OPTIONAL_TEXT_FIELDS = [
39
41
  ]
40
42
 
41
43
 
44
+ def _json_schema_extra(schema: JsonDict) -> None:
45
+ """
46
+ Mutate the generated JSON Schema in place to:
47
+ - allow `null` for `display_labels`
48
+ - mark the non-null branch as deprecated
49
+ """
50
+ props = schema.get("properties")
51
+ if not isinstance(props, dict):
52
+ return
53
+ dl = props.get("display_labels")
54
+ if not isinstance(dl, dict):
55
+ return
56
+
57
+ if "anyOf" in dl:
58
+ dl["anyOf"] = [
59
+ {
60
+ "type": "array",
61
+ "items": {
62
+ "type": "string",
63
+ "deprecationMessage": "display_labels are deprecated use display_label instead",
64
+ },
65
+ },
66
+ {"type": "null"},
67
+ ]
68
+
69
+
42
70
  class BaseNodeSchema(GeneratedBaseNodeSchema):
43
71
  _exclude_from_hash: list[str] = ["attributes", "relationships"]
44
72
  _sort_by: list[str] = ["namespace", "name"]
45
73
 
74
+ model_config = ConfigDict(extra="forbid", json_schema_extra=_json_schema_extra)
75
+
76
+ @property
77
+ def is_schema_node(self) -> bool:
78
+ """Tell if this node represent a part of the schema. Not to confuse this with `is_node_schema`."""
79
+ return self.namespace == "Schema"
80
+
46
81
  @property
47
82
  def is_node_schema(self) -> bool:
48
83
  return False
@@ -240,6 +275,11 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
240
275
  return None
241
276
 
242
277
  def get_attribute(self, name: str) -> AttributeSchema:
278
+ if name == "human_friendly_id":
279
+ return AttributeSchema(name="human_friendly_id", kind="List", optional=True, branch=self.branch)
280
+ if name == "display_label":
281
+ return AttributeSchema(name="display_label", kind="Text", optional=True, branch=self.branch)
282
+
243
283
  for item in self.attributes:
244
284
  if item.name == name:
245
285
  return item
@@ -329,7 +369,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
329
369
 
330
370
  @property
331
371
  def valid_input_names(self) -> list[str]:
332
- return self.attribute_names + self.relationship_names + NODE_METADATA_ATTRIBUTES
372
+ return self.attribute_names + self.relationship_names + NODE_METADATA_ATTRIBUTES + NODE_PROPERTY_ATTRIBUTES
333
373
 
334
374
  @property
335
375
  def valid_local_names(self) -> list[str]:
@@ -29,6 +29,7 @@ from .core import core_node, core_task_target
29
29
  from .generator import core_generator_definition, core_generator_instance
30
30
  from .graphql_query import core_graphql_query
31
31
  from .group import (
32
+ core_generator_aware_group,
32
33
  core_generator_group,
33
34
  core_graphql_query_group,
34
35
  core_group,
@@ -128,6 +129,7 @@ core_models_mixed: dict[str, list] = {
128
129
  core_group_action,
129
130
  core_standard_group,
130
131
  core_generator_group,
132
+ core_generator_aware_group,
131
133
  core_graphql_query_group,
132
134
  core_repository_group,
133
135
  builtin_tag,
@@ -29,7 +29,7 @@ core_check_definition = NodeSchema(
29
29
  Attr(name="description", kind="Text", optional=True),
30
30
  Attr(name="file_path", kind="Text"),
31
31
  Attr(name="class_name", kind="Text"),
32
- Attr(name="timeout", kind="Number", default_value=10),
32
+ Attr(name="timeout", kind="Number", default_value=60),
33
33
  Attr(name="parameters", kind="JSON", optional=True),
34
34
  ],
35
35
  relationships=[
@@ -33,6 +33,8 @@ core_generator_definition = NodeSchema(
33
33
  Attr(name="file_path", kind="Text"),
34
34
  Attr(name="class_name", kind="Text"),
35
35
  Attr(name="convert_query_response", kind="Boolean", optional=True, default_value=False),
36
+ Attr(name="execute_in_proposed_change", kind="Boolean", optional=True, default_value=True),
37
+ Attr(name="execute_after_merge", kind="Boolean", optional=True, default_value=True),
36
38
  ],
37
39
  relationships=[
38
40
  Rel(
@@ -70,10 +70,10 @@ core_standard_group = NodeSchema(
70
70
  core_generator_group = NodeSchema(
71
71
  name="GeneratorGroup",
72
72
  namespace="Core",
73
- description="Group of nodes that are created by a generator.",
73
+ description="Group of nodes that are created by a generator. (local)",
74
74
  include_in_menu=False,
75
75
  icon="mdi:state-machine",
76
- label="Generator Group",
76
+ label="Generator Group (local)",
77
77
  default_filter="name__value",
78
78
  order_by=["name__value"],
79
79
  display_labels=["name__value"],
@@ -82,6 +82,20 @@ core_generator_group = NodeSchema(
82
82
  generate_profile=False,
83
83
  )
84
84
 
85
+ core_generator_aware_group = NodeSchema(
86
+ name="GeneratorAwareGroup",
87
+ namespace="Core",
88
+ description="Group of nodes that are created by a generator. (Aware)",
89
+ include_in_menu=False,
90
+ icon="mdi:state-machine",
91
+ label="Generator Group (aware)",
92
+ default_filter="name__value",
93
+ order_by=["name__value"],
94
+ display_labels=["name__value"],
95
+ branch=BranchSupportType.AWARE,
96
+ inherit_from=[InfrahubKind.GENERICGROUP],
97
+ generate_profile=False,
98
+ )
85
99
 
86
100
  core_graphql_query_group = NodeSchema(
87
101
  name="GraphQLQueryGroup",
@@ -282,5 +282,12 @@ core_generic_repository = GenericSchema(
282
282
  cardinality=Cardinality.MANY,
283
283
  order_weight=12000,
284
284
  ),
285
+ Rel(
286
+ name="groups_objects",
287
+ peer=InfrahubKind.REPOSITORYGROUP,
288
+ optional=True,
289
+ cardinality=Cardinality.MANY,
290
+ order_weight=13000,
291
+ ),
285
292
  ],
286
293
  )
@@ -29,7 +29,7 @@ core_transform = GenericSchema(
29
29
  Attr(name="name", kind="Text", unique=True),
30
30
  Attr(name="label", kind="Text", optional=True),
31
31
  Attr(name="description", kind="Text", optional=True),
32
- Attr(name="timeout", kind="Number", default_value=10),
32
+ Attr(name="timeout", kind="Number", default_value=60),
33
33
  ],
34
34
  relationships=[
35
35
  Rel(
@@ -179,6 +179,7 @@ class SchemaNode(BaseModel):
179
179
  default_filter: str | None = None
180
180
  attributes: list[SchemaAttribute]
181
181
  relationships: list[SchemaRelationship]
182
+ display_label: str | None = None
182
183
  display_labels: list[str]
183
184
  uniqueness_constraints: list[list[str]] | None = None
184
185
 
@@ -195,6 +196,7 @@ class SchemaNode(BaseModel):
195
196
  if attribute.name not in ["id", "attributes", "relationships"]
196
197
  ],
197
198
  "relationships": [relationship.to_dict() for relationship in self.relationships],
199
+ "display_label": self.display_label,
198
200
  "display_labels": self.display_labels,
199
201
  "uniqueness_constraints": self.uniqueness_constraints,
200
202
  }
@@ -294,11 +296,18 @@ base_node_schema = SchemaNode(
294
296
  optional=True,
295
297
  extra={"update": UpdateSupport.ALLOWED},
296
298
  ),
299
+ SchemaAttribute(
300
+ name="display_label",
301
+ kind="Text",
302
+ description="Attribute or Jinja2 template to use to generate the display label",
303
+ optional=True,
304
+ extra={"update": UpdateSupport.ALLOWED},
305
+ ),
297
306
  SchemaAttribute(
298
307
  name="display_labels",
299
308
  kind="List",
300
309
  internal_kind=str,
301
- description="List of attributes to use to generate the display label",
310
+ description="List of attributes to use to generate the display label (deprecated)",
302
311
  optional=True,
303
312
  extra={"update": UpdateSupport.ALLOWED},
304
313
  ),
@@ -559,7 +568,7 @@ attribute_schema = SchemaNode(
559
568
  "Mainly relevant for internal object.",
560
569
  default_value=False,
561
570
  optional=True,
562
- extra={"update": UpdateSupport.ALLOWED},
571
+ extra={"update": UpdateSupport.MIGRATION_REQUIRED},
563
572
  ),
564
573
  SchemaAttribute(
565
574
  name="unique",
@@ -576,7 +585,7 @@ attribute_schema = SchemaNode(
576
585
  default_value=False,
577
586
  override_default_value=False,
578
587
  optional=True,
579
- extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
588
+ extra={"update": UpdateSupport.MIGRATION_REQUIRED},
580
589
  ),
581
590
  SchemaAttribute(
582
591
  name="branch",
@@ -78,7 +78,7 @@ class GeneratedAttributeSchema(HashableModel):
78
78
  read_only: bool = Field(
79
79
  default=False,
80
80
  description="Set the attribute as Read-Only, users won't be able to change its value. Mainly relevant for internal object.",
81
- json_schema_extra={"update": "allowed"},
81
+ json_schema_extra={"update": "migration_required"},
82
82
  )
83
83
  unique: bool = Field(
84
84
  default=False,
@@ -88,7 +88,7 @@ class GeneratedAttributeSchema(HashableModel):
88
88
  optional: bool = Field(
89
89
  default=False,
90
90
  description="Indicate if this attribute is mandatory or optional.",
91
- json_schema_extra={"update": "validate_constraint"},
91
+ json_schema_extra={"update": "migration_required"},
92
92
  )
93
93
  branch: BranchSupportType | None = Field(
94
94
  default=None,
@@ -58,9 +58,14 @@ class GeneratedBaseNodeSchema(HashableModel):
58
58
  description="Human friendly and unique identifier for the object.",
59
59
  json_schema_extra={"update": "allowed"},
60
60
  )
61
+ display_label: str | None = Field(
62
+ default=None,
63
+ description="Attribute or Jinja2 template to use to generate the display label",
64
+ json_schema_extra={"update": "allowed"},
65
+ )
61
66
  display_labels: list[str] | None = Field(
62
67
  default=None,
63
- description="List of attributes to use to generate the display label",
68
+ description="List of attributes to use to generate the display label (deprecated)",
64
69
  json_schema_extra={"update": "allowed"},
65
70
  )
66
71
  include_in_menu: bool | None = Field(
@@ -163,6 +163,9 @@ class SchemaManager(NodeManager):
163
163
  self._branches[name] = schema
164
164
  self._branch_hash_by_name[name] = schema.get_hash()
165
165
 
166
+ def has_schema_branch(self, name: str) -> bool:
167
+ return name in self._branches
168
+
166
169
  def process_schema_branch(self, name: str) -> None:
167
170
  schema_branch = self.get_schema_branch(name=name)
168
171
  schema_branch.process()
@@ -90,6 +90,7 @@ class NodeSchema(GeneratedNodeSchema):
90
90
 
91
91
  properties_to_inherit = [
92
92
  "human_friendly_id",
93
+ "display_label",
93
94
  "display_labels",
94
95
  "default_filter",
95
96
  "menu_placement",
@@ -91,7 +91,6 @@ class RelationshipSchema(GeneratedRelationshipSchema):
91
91
  include_match: bool = True,
92
92
  param_prefix: str | None = None,
93
93
  partial_match: bool = False,
94
- support_profiles: bool = False, # noqa: ARG002
95
94
  ) -> tuple[list[QueryElement], dict[str, Any], list[str]]:
96
95
  """Generate Query String Snippet to filter the right node."""
97
96