infrahub-server 1.6.3__py3-none-any.whl → 1.7.0b0__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 (161) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/schema.py +3 -1
  3. infrahub/artifacts/tasks.py +1 -0
  4. infrahub/auth.py +2 -2
  5. infrahub/cli/db.py +6 -6
  6. infrahub/computed_attribute/gather.py +3 -4
  7. infrahub/computed_attribute/tasks.py +23 -6
  8. infrahub/config.py +8 -0
  9. infrahub/constants/enums.py +12 -0
  10. infrahub/core/account.py +5 -8
  11. infrahub/core/attribute.py +106 -108
  12. infrahub/core/branch/models.py +44 -71
  13. infrahub/core/branch/tasks.py +5 -3
  14. infrahub/core/changelog/diff.py +1 -20
  15. infrahub/core/changelog/models.py +0 -7
  16. infrahub/core/constants/__init__.py +17 -0
  17. infrahub/core/constants/database.py +0 -1
  18. infrahub/core/constants/schema.py +0 -1
  19. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  20. infrahub/core/diff/data_check_synchronizer.py +3 -2
  21. infrahub/core/diff/enricher/cardinality_one.py +1 -1
  22. infrahub/core/diff/merger/merger.py +27 -1
  23. infrahub/core/diff/merger/serializer.py +3 -10
  24. infrahub/core/diff/model/diff.py +1 -1
  25. infrahub/core/diff/query/merge.py +376 -135
  26. infrahub/core/graph/__init__.py +1 -1
  27. infrahub/core/graph/constraints.py +2 -2
  28. infrahub/core/graph/schema.py +2 -12
  29. infrahub/core/manager.py +132 -126
  30. infrahub/core/metadata/__init__.py +0 -0
  31. infrahub/core/metadata/interface.py +37 -0
  32. infrahub/core/metadata/model.py +31 -0
  33. infrahub/core/metadata/query/__init__.py +0 -0
  34. infrahub/core/metadata/query/node_metadata.py +301 -0
  35. infrahub/core/migrations/graph/__init__.py +4 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +3 -8
  37. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  38. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  39. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  40. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  41. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  42. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  43. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  44. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  45. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  46. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +38 -0
  47. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  48. infrahub/core/migrations/query/attribute_add.py +17 -6
  49. infrahub/core/migrations/query/attribute_remove.py +19 -5
  50. infrahub/core/migrations/query/attribute_rename.py +21 -5
  51. infrahub/core/migrations/query/node_duplicate.py +19 -4
  52. infrahub/core/migrations/schema/attribute_kind_update.py +25 -7
  53. infrahub/core/migrations/schema/attribute_supports_profile.py +3 -1
  54. infrahub/core/migrations/schema/models.py +3 -0
  55. infrahub/core/migrations/schema/node_attribute_add.py +4 -1
  56. infrahub/core/migrations/schema/node_remove.py +24 -2
  57. infrahub/core/migrations/schema/tasks.py +4 -1
  58. infrahub/core/migrations/shared.py +13 -6
  59. infrahub/core/models.py +6 -6
  60. infrahub/core/node/__init__.py +156 -57
  61. infrahub/core/node/create.py +7 -3
  62. infrahub/core/node/standard.py +100 -14
  63. infrahub/core/property.py +0 -1
  64. infrahub/core/protocols_base.py +6 -2
  65. infrahub/core/query/__init__.py +6 -7
  66. infrahub/core/query/attribute.py +161 -46
  67. infrahub/core/query/branch.py +57 -69
  68. infrahub/core/query/diff.py +4 -4
  69. infrahub/core/query/node.py +618 -180
  70. infrahub/core/query/relationship.py +449 -300
  71. infrahub/core/query/standard_node.py +25 -5
  72. infrahub/core/query/utils.py +2 -4
  73. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  74. infrahub/core/relationship/model.py +293 -139
  75. infrahub/core/schema/attribute_parameters.py +1 -28
  76. infrahub/core/schema/attribute_schema.py +17 -11
  77. infrahub/core/schema/manager.py +63 -43
  78. infrahub/core/schema/relationship_schema.py +6 -2
  79. infrahub/core/schema/schema_branch.py +48 -76
  80. infrahub/core/task/task.py +4 -2
  81. infrahub/core/utils.py +0 -22
  82. infrahub/core/validators/attribute/kind.py +2 -5
  83. infrahub/core/validators/determiner.py +3 -3
  84. infrahub/database/__init__.py +3 -3
  85. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  86. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  87. infrahub/dependencies/registry.py +2 -0
  88. infrahub/display_labels/tasks.py +12 -3
  89. infrahub/git/integrator.py +18 -18
  90. infrahub/git/tasks.py +1 -1
  91. infrahub/graphql/app.py +2 -2
  92. infrahub/graphql/constants.py +3 -0
  93. infrahub/graphql/context.py +1 -1
  94. infrahub/graphql/initialization.py +11 -0
  95. infrahub/graphql/loaders/account.py +134 -0
  96. infrahub/graphql/loaders/node.py +5 -12
  97. infrahub/graphql/loaders/peers.py +5 -7
  98. infrahub/graphql/manager.py +158 -18
  99. infrahub/graphql/metadata.py +91 -0
  100. infrahub/graphql/models.py +33 -3
  101. infrahub/graphql/mutations/account.py +5 -5
  102. infrahub/graphql/mutations/attribute.py +0 -2
  103. infrahub/graphql/mutations/branch.py +9 -5
  104. infrahub/graphql/mutations/computed_attribute.py +1 -1
  105. infrahub/graphql/mutations/display_label.py +1 -1
  106. infrahub/graphql/mutations/hfid.py +1 -1
  107. infrahub/graphql/mutations/ipam.py +4 -6
  108. infrahub/graphql/mutations/main.py +9 -4
  109. infrahub/graphql/mutations/profile.py +16 -22
  110. infrahub/graphql/mutations/proposed_change.py +4 -4
  111. infrahub/graphql/mutations/relationship.py +40 -10
  112. infrahub/graphql/mutations/repository.py +14 -12
  113. infrahub/graphql/mutations/schema.py +2 -2
  114. infrahub/graphql/queries/branch.py +62 -6
  115. infrahub/graphql/queries/diff/tree.py +5 -5
  116. infrahub/graphql/resolvers/account_metadata.py +84 -0
  117. infrahub/graphql/resolvers/ipam.py +6 -8
  118. infrahub/graphql/resolvers/many_relationship.py +77 -35
  119. infrahub/graphql/resolvers/resolver.py +16 -12
  120. infrahub/graphql/resolvers/single_relationship.py +87 -23
  121. infrahub/graphql/subscription/graphql_query.py +2 -0
  122. infrahub/graphql/types/__init__.py +0 -1
  123. infrahub/graphql/types/attribute.py +10 -5
  124. infrahub/graphql/types/branch.py +40 -53
  125. infrahub/graphql/types/enums.py +3 -0
  126. infrahub/graphql/types/metadata.py +28 -0
  127. infrahub/graphql/types/node.py +22 -2
  128. infrahub/graphql/types/relationship.py +10 -2
  129. infrahub/graphql/types/standard_node.py +4 -3
  130. infrahub/hfid/tasks.py +12 -3
  131. infrahub/profiles/gather.py +56 -0
  132. infrahub/profiles/mandatory_fields_checker.py +116 -0
  133. infrahub/profiles/models.py +66 -0
  134. infrahub/profiles/node_applier.py +153 -12
  135. infrahub/profiles/queries/get_profile_data.py +143 -31
  136. infrahub/profiles/tasks.py +79 -27
  137. infrahub/profiles/triggers.py +22 -0
  138. infrahub/proposed_change/tasks.py +4 -1
  139. infrahub/tasks/artifact.py +1 -0
  140. infrahub/transformations/tasks.py +2 -2
  141. infrahub/trigger/catalogue.py +2 -0
  142. infrahub/trigger/models.py +1 -0
  143. infrahub/trigger/setup.py +3 -3
  144. infrahub/trigger/tasks.py +3 -0
  145. infrahub/validators/tasks.py +1 -0
  146. infrahub/webhook/models.py +1 -1
  147. infrahub/webhook/tasks.py +1 -1
  148. infrahub/workers/dependencies.py +9 -3
  149. infrahub/workers/infrahub_async.py +13 -4
  150. infrahub/workflows/catalogue.py +19 -0
  151. infrahub_sdk/node/constants.py +1 -0
  152. infrahub_sdk/node/related_node.py +13 -4
  153. infrahub_sdk/node/relationship.py +8 -0
  154. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/METADATA +17 -16
  155. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/RECORD +161 -143
  156. infrahub_testcontainers/container.py +3 -3
  157. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  158. infrahub_testcontainers/docker-compose.test.yml +13 -5
  159. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/WHEEL +0 -0
  160. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/entry_points.txt +0 -0
  161. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
- from typing import Any, Self
4
+ from typing import Self
5
5
 
6
6
  from pydantic import ConfigDict, Field, model_validator
7
7
 
@@ -24,33 +24,6 @@ def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParamete
24
24
  class AttributeParameters(HashableModel):
25
25
  model_config = ConfigDict(extra="forbid")
26
26
 
27
- @classmethod
28
- def convert_from(cls, source: AttributeParameters) -> Self:
29
- """Convert from another AttributeParameters subclass.
30
-
31
- Args:
32
- source: The source AttributeParameters instance to convert from
33
-
34
- Returns:
35
- A new instance of the target class with compatible fields populated
36
- """
37
- source_data = source.model_dump()
38
- return cls.convert_from_dict(source_data=source_data)
39
-
40
- @classmethod
41
- def convert_from_dict(cls, source_data: dict[str, Any]) -> Self:
42
- """Convert from a dictionary to the target class.
43
-
44
- Args:
45
- source_data: The source dictionary to convert from
46
-
47
- Returns:
48
- A new instance of the target class with compatible fields populated
49
- """
50
- target_fields = set(cls.model_fields.keys())
51
- filtered_data = {k: v for k, v in source_data.items() if k in target_fields}
52
- return cls(**filtered_data)
53
-
54
27
 
55
28
  class TextAttributeParameters(AttributeParameters):
56
29
  regex: str | None = Field(
@@ -70,7 +70,7 @@ class AttributeSchema(GeneratedAttributeSchema):
70
70
 
71
71
  @property
72
72
  def support_profiles(self) -> bool:
73
- return self.read_only is False and self.optional is True
73
+ return self.read_only is False and self.unique is False
74
74
 
75
75
  def get_id(self) -> str:
76
76
  if self.id is None:
@@ -114,20 +114,13 @@ class AttributeSchema(GeneratedAttributeSchema):
114
114
  @field_validator("parameters", mode="before")
115
115
  @classmethod
116
116
  def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
117
- """Override parameters class if using base AttributeParameters class and should be using a subclass.
118
-
119
- This validator handles parameter type conversion when an attribute's kind changes.
120
- Fields from the source that don't exist in the target are silently dropped.
121
- Fields with the same name in both classes are preserved.
122
- """
117
+ """Override parameters class if using base AttributeParameters class and should be using a subclass"""
123
118
  kind = info.data["kind"]
124
119
  expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
125
120
  if value is None:
126
121
  return expected_parameters_class()
127
122
  if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
128
- return expected_parameters_class.convert_from(value)
129
- if isinstance(value, dict):
130
- return expected_parameters_class.convert_from_dict(source_data=value)
123
+ return expected_parameters_class(**value.model_dump())
131
124
  return value
132
125
 
133
126
  @model_validator(mode="after")
@@ -175,7 +168,7 @@ class AttributeSchema(GeneratedAttributeSchema):
175
168
 
176
169
  def update_from_generic(self, other: AttributeSchema) -> None:
177
170
  fields_to_exclude = ("id", "order_weight", "branch", "inherited")
178
- for name in self.model_fields:
171
+ for name in self.__class__.model_fields:
179
172
  if name in fields_to_exclude:
180
173
  continue
181
174
  if getattr(self, name) != getattr(other, name):
@@ -245,6 +238,19 @@ class TextAttributeSchema(AttributeSchema):
245
238
  json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
246
239
  )
247
240
 
241
+ @model_validator(mode="after")
242
+ def reconcile_parameters(self) -> Self:
243
+ if self.regex != self.parameters.regex:
244
+ final_regex = self.parameters.regex if self.parameters.regex is not None else self.regex
245
+ self.regex = self.parameters.regex = final_regex
246
+ if self.min_length != self.parameters.min_length:
247
+ final_min_length = self.parameters.min_length if self.parameters.min_length is not None else self.min_length
248
+ self.min_length = self.parameters.min_length = final_min_length
249
+ if self.max_length != self.parameters.max_length:
250
+ final_max_length = self.parameters.max_length if self.parameters.max_length is not None else self.max_length
251
+ self.max_length = self.parameters.max_length = final_max_length
252
+ return self
253
+
248
254
  def get_regex(self) -> str | None:
249
255
  return self.parameters.regex
250
256
 
@@ -6,6 +6,7 @@ from cachetools import LRUCache
6
6
  from infrahub_sdk.schema import BranchSchema as SDKBranchSchema
7
7
 
8
8
  from infrahub import lock
9
+ from infrahub.core.constants import SYSTEM_USER_ID, MetadataOptions
9
10
  from infrahub.core.manager import NodeManager
10
11
  from infrahub.core.models import (
11
12
  HashableModelDiff,
@@ -178,15 +179,18 @@ class SchemaManager(NodeManager):
178
179
  diff: SchemaDiff | None = None,
179
180
  limit: list[str] | None = None,
180
181
  update_db: bool = True,
182
+ user_id: str = SYSTEM_USER_ID,
181
183
  ) -> None:
182
184
  branch = await registry.get_branch(branch=branch, db=db)
183
185
 
184
186
  updated_schema = None
185
187
  if update_db:
186
188
  if diff:
187
- schema_diff = await self.update_schema_to_db(schema=schema, db=db, branch=branch, diff=diff)
189
+ schema_diff = await self.update_schema_to_db(
190
+ schema=schema, db=db, branch=branch, diff=diff, user_id=user_id
191
+ )
188
192
  else:
189
- await self.load_schema_to_db(schema=schema, db=db, branch=branch, limit=limit)
193
+ await self.load_schema_to_db(schema=schema, db=db, branch=branch, limit=limit, user_id=user_id)
190
194
  # After updating the schema into the db
191
195
  # we need to pull a fresh version because some default value are managed/generated within the node object
192
196
  schema_diff = None
@@ -216,6 +220,7 @@ class SchemaManager(NodeManager):
216
220
  schema: SchemaBranch,
217
221
  db: InfrahubDatabase,
218
222
  diff: SchemaDiff,
223
+ user_id: str,
219
224
  branch: Branch | str | None = None,
220
225
  ) -> SchemaBranchDiff:
221
226
  """Load all nodes, generics and groups from a SchemaRoot object into the database."""
@@ -226,7 +231,7 @@ class SchemaManager(NodeManager):
226
231
  added_generics = []
227
232
  for item_kind in diff.added.keys():
228
233
  item = schema.get(name=item_kind, duplicate=False)
229
- node = await self.load_node_to_db(node=item, branch=branch, db=db)
234
+ node = await self.load_node_to_db(node=item, branch=branch, db=db, user_id=user_id)
230
235
  schema.set(name=item_kind, schema=node)
231
236
  if item.is_node_schema:
232
237
  added_nodes.append(item_kind)
@@ -238,9 +243,11 @@ class SchemaManager(NodeManager):
238
243
  for item_kind, item_diff in diff.changed.items():
239
244
  item = schema.get(name=item_kind, duplicate=False)
240
245
  if item_diff:
241
- node = await self.update_node_in_db_based_on_diff(node=item, branch=branch, db=db, diff=item_diff)
246
+ node = await self.update_node_in_db_based_on_diff(
247
+ node=item, branch=branch, db=db, diff=item_diff, user_id=user_id
248
+ )
242
249
  else:
243
- node = await self.update_node_in_db(node=item, branch=branch, db=db)
250
+ node = await self.update_node_in_db(node=item, branch=branch, db=db, user_id=user_id)
244
251
  schema.set(name=item_kind, schema=node)
245
252
  if item.is_node_schema:
246
253
  changed_nodes.append(item_kind)
@@ -251,7 +258,7 @@ class SchemaManager(NodeManager):
251
258
  removed_generics = []
252
259
  for item_kind in diff.removed.keys():
253
260
  item = schema.get(name=item_kind, duplicate=False)
254
- node = await self.delete_node_in_db(node=item, branch=branch, db=db)
261
+ node = await self.delete_node_in_db(node=item, branch=branch, db=db, user_id=user_id)
255
262
  schema.delete(name=item_kind)
256
263
  if item.is_node_schema:
257
264
  removed_nodes.append(item_kind)
@@ -273,6 +280,7 @@ class SchemaManager(NodeManager):
273
280
  db: InfrahubDatabase,
274
281
  branch: Branch | str | None = None,
275
282
  limit: list[str] | None = None,
283
+ user_id: str = SYSTEM_USER_ID,
276
284
  ) -> None:
277
285
  """Load all nodes, generics and groups from a SchemaRoot object into the database."""
278
286
 
@@ -283,16 +291,17 @@ class SchemaManager(NodeManager):
283
291
  continue
284
292
  item = schema.get(name=item_kind, duplicate=False)
285
293
  if not item.id:
286
- node = await self.load_node_to_db(node=item, branch=branch, db=db)
294
+ node = await self.load_node_to_db(node=item, branch=branch, db=db, user_id=user_id)
287
295
  schema.set(name=item_kind, schema=node)
288
296
  else:
289
- node = await self.update_node_in_db(node=item, branch=branch, db=db)
297
+ node = await self.update_node_in_db(node=item, branch=branch, db=db, user_id=user_id)
290
298
  schema.set(name=item_kind, schema=node)
291
299
 
292
300
  async def load_node_to_db(
293
301
  self,
294
302
  node: NodeSchema | GenericSchema,
295
303
  db: InfrahubDatabase,
304
+ user_id: str,
296
305
  branch: Branch | str | None = None,
297
306
  ) -> NodeSchema | GenericSchema:
298
307
  """Load a Node with its attributes and its relationships to the database."""
@@ -313,7 +322,7 @@ class SchemaManager(NodeManager):
313
322
  schema_dict = node.model_dump(exclude={"id", "state", "filters", "relationships", "attributes"})
314
323
  obj = await Node.init(schema=node_schema, branch=branch, db=db)
315
324
  await obj.new(**schema_dict, db=db)
316
- await obj.save(db=db)
325
+ await obj.save(db=db, user_id=user_id)
317
326
  new_node.id = obj.id
318
327
 
319
328
  # Then create the Attributes and the relationships
@@ -324,7 +333,7 @@ class SchemaManager(NodeManager):
324
333
  for item in node.attributes:
325
334
  if item.inherited is False:
326
335
  new_attr = await self.create_attribute_in_db(
327
- schema=attribute_schema, item=item, parent=obj, branch=branch, db=db
336
+ schema=attribute_schema, item=item, parent=obj, branch=branch, db=db, user_id=user_id
328
337
  )
329
338
  else:
330
339
  new_attr = item.duplicate()
@@ -333,7 +342,7 @@ class SchemaManager(NodeManager):
333
342
  for item in node.relationships:
334
343
  if item.inherited is False:
335
344
  new_rel = await self.create_relationship_in_db(
336
- schema=relationship_schema, item=item, parent=obj, branch=branch, db=db
345
+ schema=relationship_schema, item=item, parent=obj, branch=branch, db=db, user_id=user_id
337
346
  )
338
347
  else:
339
348
  new_rel = item.duplicate()
@@ -347,6 +356,7 @@ class SchemaManager(NodeManager):
347
356
  self,
348
357
  db: InfrahubDatabase,
349
358
  node: NodeSchema | GenericSchema,
359
+ user_id: str,
350
360
  branch: Branch | str | None = None,
351
361
  ) -> NodeSchema | GenericSchema:
352
362
  """Update a Node with its attributes and its relationships in the database."""
@@ -374,7 +384,7 @@ class SchemaManager(NodeManager):
374
384
  await obj.relationships.update(
375
385
  db=db, data=[item.id for item in node.local_relationships if item.id and item.name != "profiles"]
376
386
  )
377
- await obj.save(db=db)
387
+ await obj.save(db=db, user_id=user_id)
378
388
 
379
389
  # Then Update the Attributes and the relationships
380
390
 
@@ -382,25 +392,24 @@ class SchemaManager(NodeManager):
382
392
  ids=[item.id for item in node.local_attributes + node.local_relationships if item.id],
383
393
  db=db,
384
394
  branch=branch,
385
- include_owner=True,
386
- include_source=True,
395
+ include_metadata=MetadataOptions.LINKED_NODES,
387
396
  )
388
397
 
389
398
  for item in node.local_attributes:
390
399
  if item.id and item.id in items:
391
- await self.update_attribute_in_db(item=item, attr=items[item.id], db=db)
400
+ await self.update_attribute_in_db(item=item, attr=items[item.id], db=db, user_id=user_id)
392
401
  elif not item.id:
393
402
  new_attr = await self.create_attribute_in_db(
394
- schema=attribute_schema, item=item, branch=branch, db=db, parent=obj
403
+ schema=attribute_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
395
404
  )
396
405
  new_node.attributes.append(new_attr)
397
406
 
398
407
  for item in node.local_relationships:
399
408
  if item.id and item.id in items:
400
- await self.update_relationship_in_db(item=item, rel=items[item.id], db=db)
409
+ await self.update_relationship_in_db(item=item, rel=items[item.id], db=db, user_id=user_id)
401
410
  elif not item.id:
402
411
  new_rel = await self.create_relationship_in_db(
403
- schema=relationship_schema, item=item, branch=branch, db=db, parent=obj
412
+ schema=relationship_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
404
413
  )
405
414
  new_node.relationships.append(new_rel)
406
415
 
@@ -413,6 +422,7 @@ class SchemaManager(NodeManager):
413
422
  db: InfrahubDatabase,
414
423
  diff: HashableModelDiff,
415
424
  node: NodeSchema | GenericSchema,
425
+ user_id: str,
416
426
  branch: Branch | str | None = None,
417
427
  ) -> NodeSchema | GenericSchema:
418
428
  """Update a Node with its attributes and its relationships in the database based on a HashableModelDiff."""
@@ -466,8 +476,7 @@ class SchemaManager(NodeManager):
466
476
  ids=list(item_ids),
467
477
  db=db,
468
478
  branch=branch,
469
- include_owner=True,
470
- include_source=True,
479
+ include_metadata=MetadataOptions.LINKED_NODES,
471
480
  )
472
481
  if missing_field_names:
473
482
  missing_attrs = await self.query(
@@ -475,16 +484,14 @@ class SchemaManager(NodeManager):
475
484
  branch=branch,
476
485
  schema=attribute_schema,
477
486
  filters={"name__values": missing_field_names, "node__id": node.id},
478
- include_owner=True,
479
- include_source=True,
487
+ include_metadata=MetadataOptions.LINKED_NODES,
480
488
  )
481
489
  missing_rels = await self.query(
482
490
  db=db,
483
491
  branch=branch,
484
492
  schema=relationship_schema,
485
493
  filters={"name__values": missing_field_names, "node__id": node.id},
486
- include_owner=True,
487
- include_source=True,
494
+ include_metadata=MetadataOptions.LINKED_NODES,
488
495
  )
489
496
  items.update({field.id: field for field in missing_attrs + missing_rels})
490
497
 
@@ -494,21 +501,21 @@ class SchemaManager(NodeManager):
494
501
  if diff_relationships:
495
502
  await obj.relationships.update(db=db, data=[item.id for item in node.local_relationships if item.id])
496
503
 
497
- await obj.save(db=db)
504
+ await obj.save(db=db, user_id=user_id)
498
505
 
499
506
  if diff_attributes:
500
507
  for item in node.local_attributes:
501
508
  # if item is in changed and has no ID, then it is being overridden from a generic and must be added
502
509
  if item.name in diff_attributes.added or (item.name in diff_attributes.changed and item.id is None):
503
510
  created_item = await self.create_attribute_in_db(
504
- schema=attribute_schema, item=item, branch=branch, db=db, parent=obj
511
+ schema=attribute_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
505
512
  )
506
513
  new_attr = new_node.get_attribute(name=item.name)
507
514
  new_attr.id = created_item.id
508
515
  elif item.name in diff_attributes.changed and item.id and item.id in items:
509
- await self.update_attribute_in_db(item=item, attr=items[item.id], db=db)
516
+ await self.update_attribute_in_db(item=item, attr=items[item.id], db=db, user_id=user_id)
510
517
  elif item.name in diff_attributes.removed and item.id and item.id in items:
511
- await items[item.id].delete(db=db)
518
+ await items[item.id].delete(db=db, user_id=user_id)
512
519
  elif (
513
520
  (item.name in diff_attributes.removed or item.name in diff_attributes.changed)
514
521
  and item.id
@@ -523,14 +530,14 @@ class SchemaManager(NodeManager):
523
530
  item.name in diff_relationships.changed and item.id is None
524
531
  ):
525
532
  created_rel = await self.create_relationship_in_db(
526
- schema=relationship_schema, item=item, branch=branch, db=db, parent=obj
533
+ schema=relationship_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
527
534
  )
528
535
  new_rel = new_node.get_relationship(name=item.name)
529
536
  new_rel.id = created_rel.id
530
537
  elif item.name in diff_relationships.changed and item.id and item.id in items:
531
- await self.update_relationship_in_db(item=item, rel=items[item.id], db=db)
538
+ await self.update_relationship_in_db(item=item, rel=items[item.id], db=db, user_id=user_id)
532
539
  elif item.name in diff_relationships.removed and item.id and item.id in items:
533
- await items[item.id].delete(db=db)
540
+ await items[item.id].delete(db=db, user_id=user_id)
534
541
  elif (
535
542
  (item.name in diff_relationships.removed or item.name in diff_relationships.changed)
536
543
  and item.id
@@ -548,7 +555,7 @@ class SchemaManager(NodeManager):
548
555
  if field_names_to_remove:
549
556
  for field_schema in items.values():
550
557
  if field_schema.name.value in field_names_to_remove:
551
- await field_schema.delete(db=db)
558
+ await field_schema.delete(db=db, user_id=user_id)
552
559
 
553
560
  # Save back the node with the (potentially) newly created IDs in the SchemaManager
554
561
  self.set(name=new_node.kind, schema=new_node, branch=branch.name)
@@ -558,6 +565,7 @@ class SchemaManager(NodeManager):
558
565
  self,
559
566
  db: InfrahubDatabase,
560
567
  node: NodeSchema | GenericSchema,
568
+ user_id: str,
561
569
  branch: Branch | str | None = None,
562
570
  ) -> None:
563
571
  """Delete the node with its attributes and relationships."""
@@ -573,47 +581,59 @@ class SchemaManager(NodeManager):
573
581
 
574
582
  # First delete the attributes and the relationships
575
583
  for attr_schema_node in (await obj.attributes.get_peers(db=db)).values():
576
- await attr_schema_node.delete(db=db)
584
+ await attr_schema_node.delete(db=db, user_id=user_id)
577
585
  for rel_schema_node in (await obj.relationships.get_peers(db=db)).values():
578
- await rel_schema_node.delete(db=db)
586
+ await rel_schema_node.delete(db=db, user_id=user_id)
579
587
 
580
- await obj.delete(db=db)
588
+ await obj.delete(db=db, user_id=user_id)
581
589
 
582
590
  @staticmethod
583
591
  async def create_attribute_in_db(
584
- schema: NodeSchema, item: AttributeSchema, branch: Branch, parent: Node, db: InfrahubDatabase
592
+ schema: NodeSchema,
593
+ item: AttributeSchema,
594
+ branch: Branch,
595
+ parent: Node,
596
+ db: InfrahubDatabase,
597
+ user_id: str,
585
598
  ) -> AttributeSchema:
586
599
  obj = await Node.init(schema=schema, branch=branch, db=db)
587
600
  await obj.new(**item.to_node(), node=parent, db=db)
588
- await obj.save(db=db)
601
+ await obj.save(db=db, user_id=user_id)
589
602
  new_item = item.duplicate()
590
603
  new_item.id = obj.id
591
604
  return new_item
592
605
 
593
606
  @staticmethod
594
- async def update_attribute_in_db(item: AttributeSchema, attr: Node, db: InfrahubDatabase) -> None:
607
+ async def update_attribute_in_db(item: AttributeSchema, attr: Node, db: InfrahubDatabase, user_id: str) -> None:
595
608
  item_dict = item.model_dump(exclude={"id", "state", "filters"})
596
609
  for key, value in item_dict.items():
597
610
  getattr(attr, key).value = value
598
- await attr.save(db=db)
611
+ await attr.save(db=db, user_id=user_id)
599
612
 
600
613
  @staticmethod
601
614
  async def create_relationship_in_db(
602
- schema: NodeSchema, item: RelationshipSchema, branch: Branch, parent: Node, db: InfrahubDatabase
615
+ schema: NodeSchema,
616
+ item: RelationshipSchema,
617
+ branch: Branch,
618
+ parent: Node,
619
+ db: InfrahubDatabase,
620
+ user_id: str,
603
621
  ) -> RelationshipSchema:
604
622
  obj = await Node.init(schema=schema, branch=branch, db=db)
605
623
  await obj.new(**item.model_dump(exclude={"id", "state", "filters"}), node=parent, db=db)
606
- await obj.save(db=db)
624
+ await obj.save(db=db, user_id=user_id)
607
625
  new_item = item.duplicate()
608
626
  new_item.id = obj.id
609
627
  return new_item
610
628
 
611
629
  @staticmethod
612
- async def update_relationship_in_db(item: RelationshipSchema, rel: Node, db: InfrahubDatabase) -> None:
630
+ async def update_relationship_in_db(
631
+ item: RelationshipSchema, rel: Node, db: InfrahubDatabase, user_id: str
632
+ ) -> None:
613
633
  item_dict = item.model_dump(exclude={"id", "state", "filters"})
614
634
  for key, value in item_dict.items():
615
635
  getattr(rel, key).value = value
616
- await rel.save(db=db)
636
+ await rel.save(db=db, user_id=user_id)
617
637
 
618
638
  async def load_schema(
619
639
  self,
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
6
6
  from pydantic import BaseModel
7
7
 
8
8
  from infrahub import config
9
- from infrahub.core.constants import RelationshipDirection
9
+ from infrahub.core.constants import RelationshipDirection, RelationshipKind
10
10
  from infrahub.core.query import QueryNode, QueryRel, QueryRelDirection
11
11
  from infrahub.core.relationship import Relationship
12
12
  from infrahub.exceptions import InitializationError
@@ -36,6 +36,10 @@ class RelationshipSchema(GeneratedRelationshipSchema):
36
36
  def is_deprecated(self) -> bool:
37
37
  return bool(self.deprecation)
38
38
 
39
+ @property
40
+ def support_profiles(self) -> bool:
41
+ return self.read_only is False and self.kind in {RelationshipKind.GENERIC, RelationshipKind.ATTRIBUTE}
42
+
39
43
  def to_dict(self) -> dict:
40
44
  data = self.model_dump(exclude_unset=True, exclude_none=True)
41
45
  for field_name, value in data.items():
@@ -75,7 +79,7 @@ class RelationshipSchema(GeneratedRelationshipSchema):
75
79
 
76
80
  def update_from_generic(self, other: RelationshipSchema) -> None:
77
81
  fields_to_exclude = ("id", "order_weight", "branch", "inherited", "filters")
78
- for name in self.model_fields:
82
+ for name in self.__class__.model_fields:
79
83
  if name in fields_to_exclude:
80
84
  continue
81
85
  if getattr(self, name) != getattr(other, name):
@@ -54,7 +54,7 @@ from infrahub.core.schema import (
54
54
  SchemaRoot,
55
55
  TemplateSchema,
56
56
  )
57
- from infrahub.core.schema.attribute_parameters import NumberPoolParameters, TextAttributeParameters
57
+ from infrahub.core.schema.attribute_parameters import NumberPoolParameters
58
58
  from infrahub.core.schema.attribute_schema import get_attribute_schema_class_for_kind
59
59
  from infrahub.core.schema.definitions.core import core_profile_schema_definition
60
60
  from infrahub.core.validators import CONSTRAINT_VALIDATOR_MAP
@@ -236,8 +236,8 @@ class SchemaBranch:
236
236
  if diff_node.has_diff:
237
237
  schema_diff.changed[key] = diff_node
238
238
 
239
- reversed_map_local = dict(map(reversed, local_kind_id_map.items()))
240
- reversed_map_other = dict(map(reversed, other_kind_id_map.items()))
239
+ reversed_map_local: dict[str | None, str] = {v: k for k, v in local_kind_id_map.items()}
240
+ reversed_map_other: dict[str | None, str] = {v: k for k, v in other_kind_id_map.items()}
241
241
 
242
242
  for shared_id in shared_ids:
243
243
  local_node = self.get(name=reversed_map_local[shared_id], duplicate=False)
@@ -501,76 +501,11 @@ class SchemaBranch:
501
501
 
502
502
  return fields or None
503
503
 
504
- def _text_attr_needs_reconciliation(self, attr: AttributeSchema) -> bool:
505
- """Check if a Text attribute needs reconciliation between deprecated fields and parameters."""
506
- if not isinstance(attr.parameters, TextAttributeParameters):
507
- return False
508
- return (
509
- attr.regex != attr.parameters.regex
510
- or attr.min_length != attr.parameters.min_length
511
- or attr.max_length != attr.parameters.max_length
512
- )
513
-
514
- def _reconcile_text_attr(self, attr: AttributeSchema) -> None:
515
- """Reconcile a single Text attribute's deprecated fields with parameters.
516
-
517
- Parameters take precedence over deprecated top-level fields when both are set.
518
- """
519
- if not isinstance(attr.parameters, TextAttributeParameters):
520
- return
521
-
522
- # Sync regex: parameters takes precedence
523
- if attr.parameters.regex is not None:
524
- attr.regex = attr.parameters.regex
525
- elif attr.regex is not None:
526
- attr.parameters.regex = attr.regex
527
-
528
- # Sync min_length: parameters takes precedence
529
- if attr.parameters.min_length is not None:
530
- attr.min_length = attr.parameters.min_length
531
- elif attr.min_length is not None:
532
- attr.parameters.min_length = attr.min_length
533
-
534
- # Sync max_length: parameters takes precedence
535
- if attr.parameters.max_length is not None:
536
- attr.max_length = attr.parameters.max_length
537
- elif attr.max_length is not None:
538
- attr.parameters.max_length = attr.max_length
539
-
540
- def _reconcile_text_attribute_parameters(self, schema: SchemaRoot | None = None) -> None:
541
- """Reconcile regex, min_length, max_length between deprecated fields and parameters for Text attributes.
542
-
543
- Args:
544
- schema: If provided, reconcile incoming schema data before merging.
545
- If None, reconcile already-loaded schemas (e.g., from database).
546
- """
547
- if schema:
548
- # Incoming schema: modify in place
549
- for item in schema.nodes + schema.generics:
550
- for attr in item.attributes:
551
- self._reconcile_text_attr(attr)
552
- return
553
-
554
- # Loaded schemas: need to duplicate before modifying
555
- for name in self.all_names:
556
- node = self.get(name=name, duplicate=False)
557
-
558
- if not any(self._text_attr_needs_reconciliation(attr) for attr in node.attributes):
559
- continue
560
-
561
- node = node.duplicate()
562
- for attr in node.attributes:
563
- self._reconcile_text_attr(attr)
564
- self.set(name=name, schema=node)
565
-
566
504
  def load_schema(self, schema: SchemaRoot) -> None:
567
505
  """Load a SchemaRoot object and store all NodeSchema or GenericSchema.
568
506
 
569
507
  In the current implementation, if a schema object present in the SchemaRoot already exist, it will be overwritten.
570
508
  """
571
- # Reconcile deprecated text attribute parameters before merging
572
- self._reconcile_text_attribute_parameters(schema)
573
-
574
509
  for item in schema.nodes + schema.generics:
575
510
  try:
576
511
  if item.id:
@@ -610,7 +545,6 @@ class SchemaBranch:
610
545
  self.generate_identifiers()
611
546
  self.process_default_values()
612
547
  self.process_deprecations()
613
- self._reconcile_text_attribute_parameters()
614
548
  self.process_cardinality_counts()
615
549
  self.process_inheritance()
616
550
  self.process_hierarchy()
@@ -2225,6 +2159,45 @@ class SchemaBranch:
2225
2159
 
2226
2160
  self.set(name=node_name, schema=node)
2227
2161
 
2162
+ def add_relationships_to_profile(self, profile: ProfileSchema, node: NodeSchema | GenericSchema) -> None:
2163
+ # Remove previous relationships to account for new ones
2164
+ profile.relationships = [r for r in profile.relationships if r.kind == RelationshipKind.PROFILE]
2165
+
2166
+ for relationship in node.relationships:
2167
+ if not relationship.support_profiles:
2168
+ continue
2169
+
2170
+ # Ignore relationship if it is part of a uniqueness constraint
2171
+ ignore_relationship = False
2172
+ for constraint in node.uniqueness_constraints or []:
2173
+ if relationship.name in constraint:
2174
+ ignore_relationship = True
2175
+ break
2176
+ if ignore_relationship:
2177
+ continue
2178
+
2179
+ identifier = (
2180
+ f"profile_{relationship.identifier}"
2181
+ if relationship.identifier
2182
+ else self._generate_identifier_string(profile.kind, relationship.peer)
2183
+ )
2184
+
2185
+ profile.relationships.append(
2186
+ RelationshipSchema(
2187
+ name=relationship.name,
2188
+ peer=relationship.peer,
2189
+ kind=relationship.kind,
2190
+ cardinality=relationship.cardinality,
2191
+ direction=relationship.direction,
2192
+ branch=relationship.branch,
2193
+ identifier=identifier,
2194
+ min_count=relationship.min_count,
2195
+ max_count=relationship.max_count,
2196
+ label=relationship.label,
2197
+ inherited=False,
2198
+ )
2199
+ )
2200
+
2228
2201
  def manage_profile_schemas(self) -> None:
2229
2202
  if not self.has(name=InfrahubKind.PROFILE):
2230
2203
  # TODO: This logic is actually only for testing purposes as since 1.0.9 CoreProfile is loaded in db.
@@ -2244,6 +2217,7 @@ class SchemaBranch:
2244
2217
  continue
2245
2218
 
2246
2219
  profile = self.generate_profile_from_node(node=node)
2220
+ self.add_relationships_to_profile(profile=profile, node=node)
2247
2221
  self.set(name=profile.kind, schema=profile)
2248
2222
  profile_schema_kinds.add(profile.kind)
2249
2223
 
@@ -2318,13 +2292,13 @@ class SchemaBranch:
2318
2292
  core_name_attr = core_profile_schema.get_attribute(name="profile_name")
2319
2293
  name_attr_schema_class = get_attribute_schema_class_for_kind(kind=core_name_attr.kind)
2320
2294
  profile_name_attr = name_attr_schema_class(
2321
- **core_name_attr.model_dump(exclude=["id", "inherited"]),
2295
+ **core_name_attr.model_dump(exclude={"id", "inherited"}),
2322
2296
  )
2323
2297
  profile_name_attr.branch = node.branch
2324
2298
  core_priority_attr = core_profile_schema.get_attribute(name="profile_priority")
2325
2299
  priority_attr_schema_class = get_attribute_schema_class_for_kind(kind=core_priority_attr.kind)
2326
2300
  profile_priority_attr = priority_attr_schema_class(
2327
- **core_priority_attr.model_dump(exclude=["id", "inherited"]),
2301
+ **core_priority_attr.model_dump(exclude={"id", "inherited"}),
2328
2302
  )
2329
2303
  profile_priority_attr.branch = node.branch
2330
2304
  profile = ProfileSchema(
@@ -2369,7 +2343,7 @@ class SchemaBranch:
2369
2343
  attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
2370
2344
  attr = attr_schema_class(
2371
2345
  optional=True,
2372
- **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "default_value", "inherited"]),
2346
+ **node_attr.model_dump(exclude={"id", "unique", "optional", "read_only", "default_value", "inherited"}),
2373
2347
  )
2374
2348
  profile.attributes.append(attr)
2375
2349
 
@@ -2513,9 +2487,7 @@ class SchemaBranch:
2513
2487
  )
2514
2488
  core_name_attr = core_template_schema.get_attribute(name=OBJECT_TEMPLATE_NAME_ATTR)
2515
2489
  name_attr_schema_class = get_attribute_schema_class_for_kind(kind=core_name_attr.kind)
2516
- template_name_attr = name_attr_schema_class(
2517
- **core_name_attr.model_dump(exclude=["id", "inherited"]),
2518
- )
2490
+ template_name_attr = name_attr_schema_class(**core_name_attr.model_dump(exclude={"id", "inherited"}))
2519
2491
  template_name_attr.branch = node.branch
2520
2492
 
2521
2493
  template: TemplateSchema | GenericSchema
@@ -2574,7 +2546,7 @@ class SchemaBranch:
2574
2546
  attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
2575
2547
  attr = attr_schema_class(
2576
2548
  optional=node_attr.optional if is_autogenerated_subtemplate else True,
2577
- **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "order_weight"]),
2549
+ **node_attr.model_dump(exclude={"id", "unique", "optional", "read_only", "order_weight"}),
2578
2550
  )
2579
2551
  template.attributes.append(attr)
2580
2552