infrahub-server 1.4.13__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 (222) 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/upgrade.py +56 -9
  12. infrahub/computed_attribute/tasks.py +19 -7
  13. infrahub/config.py +7 -2
  14. infrahub/core/attribute.py +35 -24
  15. infrahub/core/branch/enums.py +1 -1
  16. infrahub/core/branch/models.py +9 -5
  17. infrahub/core/branch/needs_rebase_status.py +11 -0
  18. infrahub/core/branch/tasks.py +72 -10
  19. infrahub/core/changelog/models.py +2 -10
  20. infrahub/core/constants/__init__.py +4 -0
  21. infrahub/core/constants/infrahubkind.py +1 -0
  22. infrahub/core/convert_object_type/object_conversion.py +201 -0
  23. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  24. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  25. infrahub/core/diff/model/path.py +4 -0
  26. infrahub/core/diff/payload_builder.py +1 -1
  27. infrahub/core/diff/query/artifact.py +1 -0
  28. infrahub/core/diff/query/field_summary.py +1 -0
  29. infrahub/core/graph/__init__.py +1 -1
  30. infrahub/core/initialization.py +7 -4
  31. infrahub/core/manager.py +3 -81
  32. infrahub/core/migrations/__init__.py +3 -0
  33. infrahub/core/migrations/exceptions.py +4 -0
  34. infrahub/core/migrations/graph/__init__.py +11 -10
  35. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  37. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  38. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  39. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  40. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  41. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  42. infrahub/core/migrations/query/__init__.py +7 -8
  43. infrahub/core/migrations/query/attribute_add.py +8 -6
  44. infrahub/core/migrations/query/attribute_remove.py +134 -0
  45. infrahub/core/migrations/runner.py +54 -0
  46. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  47. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  48. infrahub/core/migrations/schema/node_attribute_add.py +26 -5
  49. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  50. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  51. infrahub/core/migrations/schema/node_remove.py +2 -1
  52. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  53. infrahub/core/migrations/shared.py +66 -19
  54. infrahub/core/models.py +2 -2
  55. infrahub/core/node/__init__.py +207 -54
  56. infrahub/core/node/create.py +53 -49
  57. infrahub/core/node/lock_utils.py +124 -0
  58. infrahub/core/node/node_property_attribute.py +230 -0
  59. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  60. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  61. infrahub/core/node/resource_manager/number_pool.py +2 -1
  62. infrahub/core/node/standard.py +1 -1
  63. infrahub/core/property.py +11 -0
  64. infrahub/core/protocols.py +8 -1
  65. infrahub/core/query/attribute.py +82 -15
  66. infrahub/core/query/ipam.py +16 -4
  67. infrahub/core/query/node.py +66 -188
  68. infrahub/core/query/relationship.py +44 -26
  69. infrahub/core/query/subquery.py +0 -8
  70. infrahub/core/relationship/model.py +69 -24
  71. infrahub/core/schema/__init__.py +56 -0
  72. infrahub/core/schema/attribute_schema.py +4 -2
  73. infrahub/core/schema/basenode_schema.py +42 -2
  74. infrahub/core/schema/definitions/core/__init__.py +2 -0
  75. infrahub/core/schema/definitions/core/check.py +1 -1
  76. infrahub/core/schema/definitions/core/generator.py +2 -0
  77. infrahub/core/schema/definitions/core/group.py +16 -2
  78. infrahub/core/schema/definitions/core/repository.py +7 -0
  79. infrahub/core/schema/definitions/core/transform.py +1 -1
  80. infrahub/core/schema/definitions/internal.py +12 -3
  81. infrahub/core/schema/generated/attribute_schema.py +2 -2
  82. infrahub/core/schema/generated/base_node_schema.py +6 -1
  83. infrahub/core/schema/manager.py +3 -0
  84. infrahub/core/schema/node_schema.py +1 -0
  85. infrahub/core/schema/relationship_schema.py +0 -1
  86. infrahub/core/schema/schema_branch.py +295 -10
  87. infrahub/core/schema/schema_branch_display.py +135 -0
  88. infrahub/core/schema/schema_branch_hfid.py +120 -0
  89. infrahub/core/validators/aggregated_checker.py +1 -1
  90. infrahub/database/graph.py +21 -0
  91. infrahub/display_labels/__init__.py +0 -0
  92. infrahub/display_labels/gather.py +48 -0
  93. infrahub/display_labels/models.py +240 -0
  94. infrahub/display_labels/tasks.py +192 -0
  95. infrahub/display_labels/triggers.py +22 -0
  96. infrahub/events/branch_action.py +27 -1
  97. infrahub/events/group_action.py +1 -1
  98. infrahub/events/node_action.py +1 -1
  99. infrahub/generators/constants.py +7 -0
  100. infrahub/generators/models.py +38 -12
  101. infrahub/generators/tasks.py +34 -16
  102. infrahub/git/base.py +38 -1
  103. infrahub/git/integrator.py +22 -14
  104. infrahub/graphql/api/dependencies.py +2 -4
  105. infrahub/graphql/api/endpoints.py +16 -6
  106. infrahub/graphql/app.py +2 -4
  107. infrahub/graphql/initialization.py +2 -3
  108. infrahub/graphql/manager.py +213 -137
  109. infrahub/graphql/middleware.py +12 -0
  110. infrahub/graphql/mutations/branch.py +16 -0
  111. infrahub/graphql/mutations/computed_attribute.py +110 -3
  112. infrahub/graphql/mutations/convert_object_type.py +44 -13
  113. infrahub/graphql/mutations/display_label.py +118 -0
  114. infrahub/graphql/mutations/generator.py +25 -7
  115. infrahub/graphql/mutations/hfid.py +125 -0
  116. infrahub/graphql/mutations/ipam.py +73 -41
  117. infrahub/graphql/mutations/main.py +61 -178
  118. infrahub/graphql/mutations/profile.py +195 -0
  119. infrahub/graphql/mutations/proposed_change.py +8 -1
  120. infrahub/graphql/mutations/relationship.py +2 -2
  121. infrahub/graphql/mutations/repository.py +22 -83
  122. infrahub/graphql/mutations/resource_manager.py +2 -2
  123. infrahub/graphql/mutations/webhook.py +1 -1
  124. infrahub/graphql/queries/resource_manager.py +1 -1
  125. infrahub/graphql/registry.py +173 -0
  126. infrahub/graphql/resolvers/resolver.py +2 -0
  127. infrahub/graphql/schema.py +8 -1
  128. infrahub/graphql/schema_sort.py +170 -0
  129. infrahub/graphql/types/branch.py +4 -1
  130. infrahub/graphql/types/enums.py +3 -0
  131. infrahub/groups/tasks.py +1 -1
  132. infrahub/hfid/__init__.py +0 -0
  133. infrahub/hfid/gather.py +48 -0
  134. infrahub/hfid/models.py +240 -0
  135. infrahub/hfid/tasks.py +191 -0
  136. infrahub/hfid/triggers.py +22 -0
  137. infrahub/lock.py +119 -42
  138. infrahub/locks/__init__.py +0 -0
  139. infrahub/locks/tasks.py +37 -0
  140. infrahub/patch/plan_writer.py +2 -2
  141. infrahub/permissions/constants.py +2 -0
  142. infrahub/profiles/__init__.py +0 -0
  143. infrahub/profiles/node_applier.py +101 -0
  144. infrahub/profiles/queries/__init__.py +0 -0
  145. infrahub/profiles/queries/get_profile_data.py +98 -0
  146. infrahub/profiles/tasks.py +63 -0
  147. infrahub/proposed_change/tasks.py +24 -5
  148. infrahub/repositories/__init__.py +0 -0
  149. infrahub/repositories/create_repository.py +113 -0
  150. infrahub/server.py +9 -1
  151. infrahub/services/__init__.py +8 -5
  152. infrahub/services/adapters/workflow/worker.py +5 -2
  153. infrahub/task_manager/event.py +5 -0
  154. infrahub/task_manager/models.py +7 -0
  155. infrahub/tasks/registry.py +6 -4
  156. infrahub/trigger/catalogue.py +4 -0
  157. infrahub/trigger/models.py +2 -0
  158. infrahub/trigger/setup.py +13 -4
  159. infrahub/trigger/tasks.py +6 -0
  160. infrahub/webhook/models.py +1 -1
  161. infrahub/workers/dependencies.py +3 -1
  162. infrahub/workers/infrahub_async.py +5 -1
  163. infrahub/workflows/catalogue.py +118 -3
  164. infrahub/workflows/initialization.py +21 -0
  165. infrahub/workflows/models.py +17 -2
  166. infrahub_sdk/branch.py +17 -8
  167. infrahub_sdk/checks.py +1 -1
  168. infrahub_sdk/client.py +376 -95
  169. infrahub_sdk/config.py +29 -2
  170. infrahub_sdk/convert_object_type.py +61 -0
  171. infrahub_sdk/ctl/branch.py +3 -0
  172. infrahub_sdk/ctl/check.py +2 -3
  173. infrahub_sdk/ctl/cli_commands.py +20 -12
  174. infrahub_sdk/ctl/config.py +8 -2
  175. infrahub_sdk/ctl/generator.py +6 -3
  176. infrahub_sdk/ctl/graphql.py +184 -0
  177. infrahub_sdk/ctl/repository.py +39 -1
  178. infrahub_sdk/ctl/schema.py +40 -10
  179. infrahub_sdk/ctl/task.py +110 -0
  180. infrahub_sdk/ctl/utils.py +4 -0
  181. infrahub_sdk/ctl/validate.py +5 -3
  182. infrahub_sdk/diff.py +4 -5
  183. infrahub_sdk/exceptions.py +2 -0
  184. infrahub_sdk/generator.py +7 -1
  185. infrahub_sdk/graphql/__init__.py +12 -0
  186. infrahub_sdk/graphql/constants.py +1 -0
  187. infrahub_sdk/graphql/plugin.py +85 -0
  188. infrahub_sdk/graphql/query.py +77 -0
  189. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  190. infrahub_sdk/graphql/utils.py +40 -0
  191. infrahub_sdk/node/attribute.py +2 -0
  192. infrahub_sdk/node/node.py +28 -20
  193. infrahub_sdk/node/relationship.py +1 -3
  194. infrahub_sdk/playback.py +1 -2
  195. infrahub_sdk/protocols.py +54 -6
  196. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  197. infrahub_sdk/pytest_plugin/utils.py +40 -0
  198. infrahub_sdk/repository.py +1 -2
  199. infrahub_sdk/schema/__init__.py +70 -4
  200. infrahub_sdk/schema/main.py +1 -0
  201. infrahub_sdk/schema/repository.py +8 -0
  202. infrahub_sdk/spec/models.py +7 -0
  203. infrahub_sdk/spec/object.py +54 -6
  204. infrahub_sdk/spec/processors/__init__.py +0 -0
  205. infrahub_sdk/spec/processors/data_processor.py +10 -0
  206. infrahub_sdk/spec/processors/factory.py +34 -0
  207. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  208. infrahub_sdk/spec/range_expansion.py +118 -0
  209. infrahub_sdk/task/models.py +6 -4
  210. infrahub_sdk/timestamp.py +18 -6
  211. infrahub_sdk/transforms.py +1 -1
  212. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
  213. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +221 -165
  214. infrahub_testcontainers/container.py +114 -2
  215. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  216. infrahub_testcontainers/docker-compose.test.yml +5 -0
  217. infrahub_testcontainers/models.py +2 -2
  218. infrahub_testcontainers/performance_test.py +4 -4
  219. infrahub/core/convert_object_type/conversion.py +0 -134
  220. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
  221. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
  222. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -350,17 +350,20 @@ class RelationshipUpdatePropertyQuery(RelationshipQuery):
350
350
 
351
351
  def __init__(
352
352
  self,
353
- properties_to_update: list[str],
354
- data: RelationshipPeerData,
353
+ rel_node_id: str,
354
+ flag_properties_to_update: dict[str, bool],
355
+ node_properties_to_update: dict[str, str],
355
356
  **kwargs,
356
357
  ):
357
- self.properties_to_update = properties_to_update
358
- self.data = data
359
-
358
+ self.rel_node_id = rel_node_id
359
+ if not flag_properties_to_update and not node_properties_to_update:
360
+ raise ValueError("Either flag_properties_to_update or node_properties_to_update must be set")
361
+ self.flag_properties_to_update = flag_properties_to_update
362
+ self.node_properties_to_update = node_properties_to_update
360
363
  super().__init__(**kwargs)
361
364
 
362
365
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
363
- self.params["rel_node_id"] = self.data.rel_node_id
366
+ self.params["rel_node_id"] = self.rel_node_id
364
367
  self.params["branch"] = self.branch.name
365
368
  self.params["branch_level"] = self.branch.hierarchy_level
366
369
  self.params["at"] = self.at.to_string()
@@ -370,36 +373,51 @@ class RelationshipUpdatePropertyQuery(RelationshipQuery):
370
373
  """
371
374
  self.add_to_query(query)
372
375
 
373
- self.query_add_all_flag_property_merge()
374
376
  self.query_add_all_node_property_merge()
377
+ self.query_add_all_flag_property_merge()
375
378
 
376
- self.query_add_all_flag_property_create()
377
379
  self.query_add_all_node_property_create()
380
+ self.query_add_all_flag_property_create()
378
381
 
379
382
  def query_add_all_flag_property_merge(self) -> None:
380
- for prop_name in self.rel._flag_properties:
381
- if prop_name in self.properties_to_update:
382
- self.query_add_flag_property_merge(name=prop_name)
383
+ for prop_name, prop_value in self.flag_properties_to_update.items():
384
+ self.query_add_flag_property_merge(name=prop_name, value=prop_value)
383
385
 
384
- def query_add_flag_property_merge(self, name: str) -> None:
386
+ def query_add_flag_property_merge(self, name: str, value: bool) -> None:
385
387
  self.add_to_query("MERGE (prop_%s:Boolean { value: $prop_%s })" % (name, name))
386
- self.params[f"prop_{name}"] = getattr(self.rel, name)
388
+ self.params[f"prop_{name}"] = value
387
389
  self.return_labels.append(f"prop_{name}")
388
390
 
389
391
  def query_add_all_node_property_merge(self) -> None:
390
- for prop_name in self.rel._node_properties:
391
- if prop_name in self.properties_to_update:
392
- self.query_add_node_property_merge(name=prop_name)
392
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
393
+ self.params.update(branch_params)
393
394
 
394
- def query_add_node_property_merge(self, name: str) -> None:
395
- self.add_to_query("MERGE (prop_%s:Node { uuid: $prop_%s })" % (name, name))
396
- self.params[f"prop_{name}"] = getattr(self.rel, f"{name}_id")
397
- self.return_labels.append(f"prop_{name}")
395
+ for prop_name, prop_value in self.node_properties_to_update.items():
396
+ self.params[f"prop_{prop_name}"] = prop_value
397
+ if self.branch.is_default or self.branch.is_global:
398
+ node_query = """
399
+ MATCH (prop_%(prop_name)s:Node {uuid: $prop_%(prop_name)s })-[r_%(prop_name)s:IS_PART_OF]->(:Root)
400
+ WHERE r_%(prop_name)s.branch IN $branch0
401
+ AND r_%(prop_name)s.status = "active"
402
+ AND r_%(prop_name)s.from <= $at AND (r_%(prop_name)s.to IS NULL OR r_%(prop_name)s.to > $at)
403
+ WITH *
404
+ LIMIT 1
405
+ """ % {"prop_name": prop_name}
406
+ else:
407
+ node_query = """
408
+ MATCH (prop_%(prop_name)s:Node {uuid: $prop_%(prop_name)s })-[r_%(prop_name)s:IS_PART_OF]->(:Root)
409
+ WHERE all(r in [r_%(prop_name)s] WHERE %(branch_filter)s)
410
+ ORDER BY r_%(prop_name)s.branch_level DESC, r_%(prop_name)s.from DESC, r_%(prop_name)s.status ASC
411
+ LIMIT 1
412
+ WITH *
413
+ WHERE r_%(prop_name)s.status = "active"
414
+ """ % {"branch_filter": branch_filter, "prop_name": prop_name}
415
+ self.add_to_query(node_query)
416
+ self.return_labels.append(f"prop_{prop_name}")
398
417
 
399
418
  def query_add_all_flag_property_create(self) -> None:
400
- for prop_name in self.rel._flag_properties:
401
- if prop_name in self.properties_to_update:
402
- self.query_add_flag_property_create(name=prop_name)
419
+ for prop_name in self.flag_properties_to_update:
420
+ self.query_add_flag_property_create(name=prop_name)
403
421
 
404
422
  def query_add_flag_property_create(self, name: str) -> None:
405
423
  query = """
@@ -411,9 +429,8 @@ class RelationshipUpdatePropertyQuery(RelationshipQuery):
411
429
  self.add_to_query(query)
412
430
 
413
431
  def query_add_all_node_property_create(self) -> None:
414
- for prop_name in self.rel._node_properties:
415
- if prop_name in self.properties_to_update:
416
- self.query_add_node_property_create(name=prop_name)
432
+ for prop_name in self.node_properties_to_update:
433
+ self.query_add_node_property_create(name=prop_name)
417
434
 
418
435
  def query_add_node_property_create(self, name: str) -> None:
419
436
  query = """
@@ -1019,6 +1036,7 @@ class RelationshipCountPerNodeQuery(Query):
1019
1036
  """ % {"branch_filter": branch_filter, "path": path}
1020
1037
 
1021
1038
  self.add_to_query(query)
1039
+ self.order_by = ["peer_node.uuid"]
1022
1040
  self.return_labels = ["peer_node.uuid", "COUNT(peer_node.uuid) as nbr_peers"]
1023
1041
 
1024
1042
  async def get_count_per_peer(self) -> dict[str, int]:
@@ -25,12 +25,8 @@ async def build_subquery_filter(
25
25
  partial_match: bool = False,
26
26
  optional_match: bool = False,
27
27
  result_prefix: str = "filter",
28
- support_profiles: bool = False,
29
28
  extra_tail_properties: dict[str, str] | None = None,
30
29
  ) -> tuple[str, dict[str, Any], str]:
31
- support_profiles = (
32
- support_profiles and field and field.is_attribute and filter_name in ("value", "values", "isnull")
33
- )
34
30
  params = {}
35
31
  prefix = f"{result_prefix}{subquery_idx}"
36
32
 
@@ -52,7 +48,6 @@ async def build_subquery_filter(
52
48
  param_prefix=prefix,
53
49
  db=db,
54
50
  partial_match=partial_match,
55
- support_profiles=support_profiles,
56
51
  )
57
52
  params.update(field_params)
58
53
 
@@ -109,10 +104,8 @@ async def build_subquery_order(
109
104
  branch: Branch = None,
110
105
  subquery_idx: int = 1,
111
106
  result_prefix: str | None = None,
112
- support_profiles: bool = False,
113
107
  extra_tail_properties: dict[str, str] | None = None,
114
108
  ) -> tuple[str, dict[str, Any], str]:
115
- support_profiles = support_profiles and field and field.is_attribute and order_by in ("value", "values")
116
109
  params = {}
117
110
  prefix = result_prefix or f"order{subquery_idx}"
118
111
 
@@ -124,7 +117,6 @@ async def build_subquery_order(
124
117
  filter_value=None,
125
118
  branch=branch,
126
119
  param_prefix=prefix,
127
- support_profiles=support_profiles,
128
120
  )
129
121
  params.update(field_params)
130
122
 
@@ -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",