infrahub-server 1.6.2__py3-none-any.whl → 1.7.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 (253) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/exceptions.py +2 -2
  3. infrahub/api/schema.py +3 -1
  4. infrahub/artifacts/tasks.py +1 -0
  5. infrahub/auth.py +2 -2
  6. infrahub/cli/db.py +54 -28
  7. infrahub/computed_attribute/gather.py +3 -4
  8. infrahub/computed_attribute/tasks.py +23 -6
  9. infrahub/config.py +8 -0
  10. infrahub/constants/enums.py +12 -0
  11. infrahub/core/account.py +12 -9
  12. infrahub/core/attribute.py +106 -108
  13. infrahub/core/branch/models.py +44 -71
  14. infrahub/core/branch/tasks.py +5 -3
  15. infrahub/core/changelog/diff.py +1 -20
  16. infrahub/core/changelog/models.py +0 -7
  17. infrahub/core/constants/__init__.py +17 -0
  18. infrahub/core/constants/database.py +0 -1
  19. infrahub/core/constants/schema.py +0 -1
  20. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  21. infrahub/core/diff/branch_differ.py +1 -1
  22. infrahub/core/diff/conflict_transferer.py +1 -1
  23. infrahub/core/diff/data_check_synchronizer.py +4 -3
  24. infrahub/core/diff/enricher/cardinality_one.py +2 -2
  25. infrahub/core/diff/enricher/hierarchy.py +1 -1
  26. infrahub/core/diff/enricher/labels.py +1 -1
  27. infrahub/core/diff/merger/merger.py +28 -2
  28. infrahub/core/diff/merger/serializer.py +3 -10
  29. infrahub/core/diff/model/diff.py +1 -1
  30. infrahub/core/diff/query/merge.py +376 -135
  31. infrahub/core/diff/repository/repository.py +3 -1
  32. infrahub/core/graph/__init__.py +1 -1
  33. infrahub/core/graph/constraints.py +3 -3
  34. infrahub/core/graph/schema.py +2 -12
  35. infrahub/core/ipam/reconciler.py +8 -6
  36. infrahub/core/ipam/utilization.py +8 -15
  37. infrahub/core/manager.py +133 -152
  38. infrahub/core/merge.py +1 -1
  39. infrahub/core/metadata/__init__.py +0 -0
  40. infrahub/core/metadata/interface.py +37 -0
  41. infrahub/core/metadata/model.py +31 -0
  42. infrahub/core/metadata/query/__init__.py +0 -0
  43. infrahub/core/metadata/query/node_metadata.py +301 -0
  44. infrahub/core/migrations/graph/__init__.py +4 -0
  45. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
  46. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -12
  47. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  48. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  49. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  50. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  51. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  52. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  53. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  54. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  55. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  56. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +1 -1
  57. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +53 -0
  58. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  59. infrahub/core/migrations/query/__init__.py +2 -2
  60. infrahub/core/migrations/query/attribute_add.py +17 -6
  61. infrahub/core/migrations/query/attribute_remove.py +19 -5
  62. infrahub/core/migrations/query/attribute_rename.py +21 -5
  63. infrahub/core/migrations/query/node_duplicate.py +19 -4
  64. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  65. infrahub/core/migrations/schema/attribute_kind_update.py +26 -6
  66. infrahub/core/migrations/schema/attribute_name_update.py +1 -1
  67. infrahub/core/migrations/schema/attribute_supports_profile.py +5 -3
  68. infrahub/core/migrations/schema/models.py +3 -0
  69. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  70. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  71. infrahub/core/migrations/schema/node_kind_update.py +1 -1
  72. infrahub/core/migrations/schema/node_remove.py +24 -2
  73. infrahub/core/migrations/schema/tasks.py +4 -1
  74. infrahub/core/migrations/shared.py +13 -6
  75. infrahub/core/models.py +6 -6
  76. infrahub/core/node/__init__.py +157 -58
  77. infrahub/core/node/base.py +9 -5
  78. infrahub/core/node/create.py +7 -3
  79. infrahub/core/node/delete_validator.py +1 -1
  80. infrahub/core/node/standard.py +100 -14
  81. infrahub/core/order.py +30 -0
  82. infrahub/core/property.py +0 -1
  83. infrahub/core/protocols.py +1 -0
  84. infrahub/core/protocols_base.py +10 -2
  85. infrahub/core/query/__init__.py +11 -6
  86. infrahub/core/query/attribute.py +164 -49
  87. infrahub/core/query/branch.py +58 -70
  88. infrahub/core/query/delete.py +1 -1
  89. infrahub/core/query/diff.py +7 -7
  90. infrahub/core/query/ipam.py +104 -43
  91. infrahub/core/query/node.py +1072 -281
  92. infrahub/core/query/relationship.py +531 -325
  93. infrahub/core/query/resource_manager.py +107 -18
  94. infrahub/core/query/standard_node.py +25 -5
  95. infrahub/core/query/utils.py +2 -4
  96. infrahub/core/relationship/constraints/count.py +1 -1
  97. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  98. infrahub/core/relationship/constraints/peer_parent.py +1 -1
  99. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  100. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  101. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  102. infrahub/core/relationship/model.py +293 -139
  103. infrahub/core/schema/attribute_parameters.py +28 -1
  104. infrahub/core/schema/attribute_schema.py +11 -17
  105. infrahub/core/schema/basenode_schema.py +3 -0
  106. infrahub/core/schema/definitions/core/__init__.py +8 -2
  107. infrahub/core/schema/definitions/core/account.py +10 -10
  108. infrahub/core/schema/definitions/core/artifact.py +14 -8
  109. infrahub/core/schema/definitions/core/check.py +10 -4
  110. infrahub/core/schema/definitions/core/generator.py +26 -6
  111. infrahub/core/schema/definitions/core/graphql_query.py +1 -1
  112. infrahub/core/schema/definitions/core/group.py +9 -2
  113. infrahub/core/schema/definitions/core/ipam.py +80 -10
  114. infrahub/core/schema/definitions/core/menu.py +41 -7
  115. infrahub/core/schema/definitions/core/permission.py +16 -2
  116. infrahub/core/schema/definitions/core/profile.py +16 -2
  117. infrahub/core/schema/definitions/core/propose_change.py +24 -4
  118. infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
  119. infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
  120. infrahub/core/schema/definitions/core/repository.py +10 -0
  121. infrahub/core/schema/definitions/core/resource_pool.py +8 -1
  122. infrahub/core/schema/definitions/core/template.py +19 -2
  123. infrahub/core/schema/definitions/core/transform.py +11 -5
  124. infrahub/core/schema/definitions/core/webhook.py +27 -9
  125. infrahub/core/schema/manager.py +63 -43
  126. infrahub/core/schema/relationship_schema.py +6 -2
  127. infrahub/core/schema/schema_branch.py +115 -11
  128. infrahub/core/task/task.py +4 -2
  129. infrahub/core/utils.py +3 -25
  130. infrahub/core/validators/aggregated_checker.py +1 -1
  131. infrahub/core/validators/attribute/choices.py +1 -1
  132. infrahub/core/validators/attribute/enum.py +1 -1
  133. infrahub/core/validators/attribute/kind.py +6 -3
  134. infrahub/core/validators/attribute/length.py +1 -1
  135. infrahub/core/validators/attribute/min_max.py +1 -1
  136. infrahub/core/validators/attribute/number_pool.py +1 -1
  137. infrahub/core/validators/attribute/optional.py +1 -1
  138. infrahub/core/validators/attribute/regex.py +1 -1
  139. infrahub/core/validators/determiner.py +3 -3
  140. infrahub/core/validators/node/attribute.py +1 -1
  141. infrahub/core/validators/node/relationship.py +1 -1
  142. infrahub/core/validators/relationship/peer.py +1 -1
  143. infrahub/database/__init__.py +4 -4
  144. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  145. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  146. infrahub/dependencies/registry.py +2 -0
  147. infrahub/display_labels/tasks.py +12 -3
  148. infrahub/git/integrator.py +18 -18
  149. infrahub/git/tasks.py +1 -1
  150. infrahub/git/utils.py +1 -1
  151. infrahub/graphql/app.py +2 -2
  152. infrahub/graphql/constants.py +3 -0
  153. infrahub/graphql/context.py +1 -1
  154. infrahub/graphql/field_extractor.py +1 -1
  155. infrahub/graphql/initialization.py +11 -0
  156. infrahub/graphql/loaders/account.py +134 -0
  157. infrahub/graphql/loaders/node.py +5 -12
  158. infrahub/graphql/loaders/peers.py +5 -7
  159. infrahub/graphql/manager.py +175 -21
  160. infrahub/graphql/metadata.py +91 -0
  161. infrahub/graphql/mutations/account.py +6 -6
  162. infrahub/graphql/mutations/attribute.py +0 -2
  163. infrahub/graphql/mutations/branch.py +9 -5
  164. infrahub/graphql/mutations/computed_attribute.py +1 -1
  165. infrahub/graphql/mutations/display_label.py +1 -1
  166. infrahub/graphql/mutations/hfid.py +1 -1
  167. infrahub/graphql/mutations/ipam.py +4 -6
  168. infrahub/graphql/mutations/main.py +9 -4
  169. infrahub/graphql/mutations/profile.py +16 -22
  170. infrahub/graphql/mutations/proposed_change.py +4 -4
  171. infrahub/graphql/mutations/relationship.py +40 -10
  172. infrahub/graphql/mutations/repository.py +14 -12
  173. infrahub/graphql/mutations/schema.py +2 -2
  174. infrahub/graphql/order.py +14 -0
  175. infrahub/graphql/queries/branch.py +62 -6
  176. infrahub/graphql/queries/diff/tree.py +5 -5
  177. infrahub/graphql/queries/resource_manager.py +25 -24
  178. infrahub/graphql/resolvers/account_metadata.py +84 -0
  179. infrahub/graphql/resolvers/ipam.py +6 -8
  180. infrahub/graphql/resolvers/many_relationship.py +77 -35
  181. infrahub/graphql/resolvers/resolver.py +59 -14
  182. infrahub/graphql/resolvers/single_relationship.py +87 -23
  183. infrahub/graphql/subscription/graphql_query.py +2 -0
  184. infrahub/graphql/types/__init__.py +0 -1
  185. infrahub/graphql/types/attribute.py +10 -5
  186. infrahub/graphql/types/branch.py +40 -53
  187. infrahub/graphql/types/enums.py +3 -0
  188. infrahub/graphql/types/metadata.py +28 -0
  189. infrahub/graphql/types/node.py +22 -2
  190. infrahub/graphql/types/relationship.py +10 -2
  191. infrahub/graphql/types/standard_node.py +12 -7
  192. infrahub/hfid/tasks.py +12 -3
  193. infrahub/lock.py +7 -0
  194. infrahub/menu/repository.py +1 -1
  195. infrahub/patch/queries/base.py +1 -1
  196. infrahub/pools/number.py +1 -8
  197. infrahub/profiles/gather.py +56 -0
  198. infrahub/profiles/mandatory_fields_checker.py +116 -0
  199. infrahub/profiles/models.py +66 -0
  200. infrahub/profiles/node_applier.py +154 -13
  201. infrahub/profiles/queries/get_profile_data.py +143 -31
  202. infrahub/profiles/tasks.py +79 -27
  203. infrahub/profiles/triggers.py +22 -0
  204. infrahub/proposed_change/action_checker.py +1 -1
  205. infrahub/proposed_change/tasks.py +4 -1
  206. infrahub/services/__init__.py +1 -1
  207. infrahub/services/adapters/cache/nats.py +1 -1
  208. infrahub/services/adapters/cache/redis.py +7 -0
  209. infrahub/tasks/artifact.py +1 -0
  210. infrahub/transformations/tasks.py +2 -2
  211. infrahub/trigger/catalogue.py +2 -0
  212. infrahub/trigger/models.py +1 -0
  213. infrahub/trigger/setup.py +3 -3
  214. infrahub/trigger/tasks.py +3 -0
  215. infrahub/validators/tasks.py +1 -0
  216. infrahub/webhook/gather.py +1 -1
  217. infrahub/webhook/models.py +1 -1
  218. infrahub/webhook/tasks.py +23 -7
  219. infrahub/workers/dependencies.py +9 -3
  220. infrahub/workers/infrahub_async.py +13 -4
  221. infrahub/workflows/catalogue.py +19 -0
  222. infrahub_sdk/analyzer.py +2 -2
  223. infrahub_sdk/branch.py +12 -39
  224. infrahub_sdk/checks.py +4 -4
  225. infrahub_sdk/client.py +36 -0
  226. infrahub_sdk/ctl/cli_commands.py +2 -1
  227. infrahub_sdk/ctl/graphql.py +15 -4
  228. infrahub_sdk/ctl/utils.py +2 -2
  229. infrahub_sdk/enums.py +6 -0
  230. infrahub_sdk/graphql/renderers.py +21 -0
  231. infrahub_sdk/graphql/utils.py +85 -0
  232. infrahub_sdk/node/attribute.py +12 -2
  233. infrahub_sdk/node/constants.py +12 -0
  234. infrahub_sdk/node/metadata.py +69 -0
  235. infrahub_sdk/node/node.py +65 -14
  236. infrahub_sdk/node/property.py +3 -0
  237. infrahub_sdk/node/related_node.py +37 -5
  238. infrahub_sdk/node/relationship.py +18 -1
  239. infrahub_sdk/operation.py +2 -2
  240. infrahub_sdk/schema/repository.py +1 -2
  241. infrahub_sdk/transforms.py +2 -2
  242. infrahub_sdk/types.py +18 -2
  243. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
  244. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +252 -231
  245. infrahub_testcontainers/container.py +3 -3
  246. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  247. infrahub_testcontainers/docker-compose.test.yml +13 -5
  248. infrahub_testcontainers/models.py +3 -3
  249. infrahub_testcontainers/performance_test.py +1 -1
  250. infrahub/graphql/models.py +0 -6
  251. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
  252. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
  253. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -14,6 +14,7 @@ from infrahub.core.constants import (
14
14
  GLOBAL_BRANCH_NAME,
15
15
  OBJECT_TEMPLATE_NAME_ATTR,
16
16
  OBJECT_TEMPLATE_RELATIONSHIP_NAME,
17
+ SYSTEM_USER_ID,
17
18
  BranchSupportType,
18
19
  ComputedAttributeKind,
19
20
  InfrahubKind,
@@ -22,8 +23,10 @@ from infrahub.core.constants import (
22
23
  RelationshipKind,
23
24
  )
24
25
  from infrahub.core.constants.schema import SchemaElementPathType
26
+ from infrahub.core.metadata.interface import MetadataInterface
27
+ from infrahub.core.metadata.model import MetadataInfo
25
28
  from infrahub.core.protocols import CoreNumberPool, CoreObjectTemplate
26
- from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
29
+ from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeUpdateMetadataQuery
27
30
  from infrahub.core.schema import (
28
31
  AttributeSchema,
29
32
  GenericSchema,
@@ -37,15 +40,14 @@ from infrahub.core.schema.attribute_parameters import NumberPoolParameters
37
40
  from infrahub.core.timestamp import Timestamp
38
41
  from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
39
42
  from infrahub.pools.models import NumberPoolLockDefinition
43
+ from infrahub.profiles.mandatory_fields_checker import ProfilesMandatoryFieldGetter
40
44
  from infrahub.types import ATTRIBUTE_TYPES
41
45
 
42
46
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
43
- from ...graphql.models import OrderModel
44
47
  from ...log import get_logger
45
48
  from ..attribute import BaseAttribute
46
49
  from ..query.relationship import RelationshipDeleteAllQuery
47
50
  from ..relationship import RelationshipManager
48
- from ..utils import update_relationships_to
49
51
  from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
50
52
  from .node_property_attribute import DisplayLabel, HumanFriendlyIdentifier
51
53
 
@@ -69,7 +71,7 @@ SchemaProtocol = TypeVar("SchemaProtocol")
69
71
  log = get_logger()
70
72
 
71
73
 
72
- class Node(BaseNode, metaclass=BaseNodeMeta):
74
+ class Node(BaseNode, MetadataInterface, metaclass=BaseNodeMeta):
73
75
  @classmethod
74
76
  def __init_subclass_with_meta__(
75
77
  cls, _meta: BaseNodeOptions | None = None, default_filter: None = None, **options: dict[str, Any]
@@ -80,13 +82,14 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
80
82
  _meta.default_filter = default_filter
81
83
  super().__init_subclass_with_meta__(_meta=_meta, **options)
82
84
 
83
- def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
85
+ def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp) -> None:
86
+ super().__init__()
84
87
  self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
85
88
  self._branch: Branch = branch
86
89
  self._at: Timestamp = at
87
90
  self._existing: bool = False
91
+ self._metadata = MetadataInfo()
88
92
 
89
- self._updated_at: Timestamp | None = None
90
93
  self.id: str = None
91
94
  self.db_id: str = None
92
95
 
@@ -94,6 +97,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
94
97
  self._owner: Node | None = None
95
98
  self._is_protected: bool = None
96
99
  self._computed_jinja2_attributes: list[str] = []
100
+ self._profile_provided_attrs: set[str] = set()
101
+ self._profile_provided_rels: set[str] = set()
97
102
 
98
103
  self._display_label: DisplayLabel | None = None
99
104
  self._human_friendly_id: HumanFriendlyIdentifier | None = None
@@ -103,6 +108,30 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
103
108
  self._relationships: list[str] = []
104
109
  self._node_changelog: NodeChangelog | None = None
105
110
 
111
+ def _set_created_at(self, value: Timestamp | None) -> None:
112
+ self._metadata.created_at = value
113
+
114
+ def _set_created_by(self, value: str | None) -> None:
115
+ self._metadata.created_by = value
116
+
117
+ def _set_updated_at(self, value: Timestamp | None) -> None:
118
+ self._metadata.updated_at = value
119
+
120
+ def _set_updated_by(self, value: str | None) -> None:
121
+ self._metadata.updated_by = value
122
+
123
+ def _get_created_at(self) -> Timestamp | None:
124
+ return self._metadata.created_at
125
+
126
+ def _get_created_by(self) -> str | None:
127
+ return self._metadata.created_by
128
+
129
+ def _get_updated_at(self) -> Timestamp | None:
130
+ return self._metadata.updated_at
131
+
132
+ def _get_updated_by(self) -> str | None:
133
+ return self._metadata.updated_by
134
+
106
135
  def get_schema(self) -> NonGenericSchemaTypes:
107
136
  return self._schema
108
137
 
@@ -120,9 +149,6 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
120
149
 
121
150
  raise InitializationError("The node has not been saved yet and doesn't have an id")
122
151
 
123
- def get_updated_at(self) -> Timestamp | None:
124
- return self._updated_at
125
-
126
152
  def get_attribute(self, name: str) -> BaseAttribute:
127
153
  attribute = getattr(self, name)
128
154
  if not isinstance(attribute, BaseAttribute):
@@ -135,6 +161,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
135
161
  raise ValueError(f"{name} is not a relationship of {self.get_kind()}")
136
162
  return relationship
137
163
 
164
+ def get_relationship_by_identifier(self, identifier: str) -> RelationshipManager:
165
+ for rel_schema in self._schema.relationships:
166
+ if rel_schema.identifier == identifier:
167
+ return self.get_relationship(rel_schema.name)
168
+ raise ValueError(f"Unable to find the relationship with the identifier {identifier} for {self.get_kind()}")
169
+
138
170
  def uses_profiles(self) -> bool:
139
171
  for attr_name in self.get_schema().attribute_names:
140
172
  try:
@@ -490,6 +522,25 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
490
522
  elif relationship_peers := await relationship.get_peers(db=db):
491
523
  fields[relationship_name] = [{"id": peer_id} for peer_id in relationship_peers]
492
524
 
525
+ async def _get_profile_provided_mandatory_fields(
526
+ self, db: InfrahubDatabase, fields: dict[str, Any]
527
+ ) -> tuple[set[str], set[str]]:
528
+ if not isinstance(self._schema, NodeSchema) or "profiles" not in fields:
529
+ return set(), set()
530
+
531
+ mandatory_attrs_to_check = [a for a in self._schema.mandatory_attribute_names if a not in fields.keys()]
532
+ mandatory_rels_to_check = [r for r in self._schema.mandatory_relationship_names if r not in fields.keys()]
533
+ if not mandatory_attrs_to_check and not mandatory_rels_to_check:
534
+ return set(), set()
535
+
536
+ profiles_mandatory_field_getter = ProfilesMandatoryFieldGetter(db=db, branch=self._branch)
537
+ return await profiles_mandatory_field_getter.get_mandatory_fields_from_profiles(
538
+ schema=self._schema,
539
+ profiles_data=fields.get("profiles"),
540
+ mandatory_attr_names=mandatory_attrs_to_check,
541
+ mandatory_rel_names=mandatory_rels_to_check,
542
+ )
543
+
493
544
  async def _process_fields(self, fields: dict, db: InfrahubDatabase, process_pools: bool = True) -> None:
494
545
  errors = []
495
546
 
@@ -511,10 +562,14 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
511
562
  # Backfill fields with the ones from the template if there's one
512
563
  await self.handle_object_template(fields=fields, db=db, errors=errors)
513
564
 
514
- # If the object is new, we need to ensure that all mandatory attributes and relationships have been provided
515
565
  if not self._existing:
566
+ (
567
+ self._profile_provided_attrs,
568
+ self._profile_provided_rels,
569
+ ) = await self._get_profile_provided_mandatory_fields(db=db, fields=fields)
570
+
516
571
  for mandatory_attr in self._schema.mandatory_attribute_names:
517
- if mandatory_attr not in fields.keys():
572
+ if mandatory_attr not in fields.keys() and mandatory_attr not in self._profile_provided_attrs:
518
573
  if self._schema.is_node_schema:
519
574
  mandatory_attribute = self._schema.get_attribute(name=mandatory_attr)
520
575
  if (
@@ -532,7 +587,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
532
587
  )
533
588
 
534
589
  for mandatory_rel in self._schema.mandatory_relationship_names:
535
- if mandatory_rel not in fields.keys():
590
+ if mandatory_rel not in fields.keys() and mandatory_rel not in self._profile_provided_rels:
536
591
  errors.append(
537
592
  ValidationError({mandatory_rel: f"{mandatory_rel} is mandatory for {self.get_kind()}"})
538
593
  )
@@ -610,6 +665,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
610
665
  attribute: BaseAttribute = getattr(self, attr_schema.name)
611
666
  await self.handle_pool(db=db, attribute=attribute, errors=errors, allocate_resources=process_pools)
612
667
 
668
+ if attr_schema.name in self._profile_provided_attrs:
669
+ continue
670
+
613
671
  if process_pools or attribute.from_pool is None:
614
672
  attribute.validate(value=attribute.value, name=attribute.name, schema=attribute.schema)
615
673
  except ValidationError as exc:
@@ -729,6 +787,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
729
787
  source=self._source,
730
788
  owner=self._owner,
731
789
  )
790
+ if isinstance(data, dict):
791
+ attr._set_created_at(data.pop("created_at", None))
792
+ attr._set_created_by(data.pop("created_by", None))
793
+ attr._set_updated_at(data.pop("updated_at", None))
794
+ attr._set_updated_by(data.pop("updated_by", None))
795
+
732
796
  return attr
733
797
 
734
798
  async def process_label(self, db: InfrahubDatabase | None = None) -> None: # noqa: ARG002
@@ -789,19 +853,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
789
853
  db: InfrahubDatabase,
790
854
  id: str | None = None,
791
855
  db_id: str | None = None,
792
- updated_at: Timestamp | str | None = None,
793
856
  **kwargs: Any,
794
857
  ) -> Self:
795
858
  self.id = id
796
859
  self.db_id = db_id
797
860
  self._existing = True
798
861
 
799
- if updated_at:
800
- kwargs["updated_at"] = (
801
- updated_at # FIXME: Allow users to use "updated_at" named attributes until we have proper metadata handling
802
- )
803
- self._updated_at = Timestamp(updated_at)
804
-
805
862
  if not self._schema.is_schema_node:
806
863
  if hfid := kwargs.pop("human_friendly_id", None):
807
864
  self._human_friendly_id = HumanFriendlyIdentifier(
@@ -815,19 +872,22 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
815
872
  await self._process_fields(db=db, fields=kwargs)
816
873
  return self
817
874
 
818
- async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> NodeChangelog:
875
+ async def _create(self, db: InfrahubDatabase, user_id: str, at: Timestamp | None = None) -> NodeChangelog:
819
876
  create_at = Timestamp(at)
877
+ self._set_created_at(create_at)
878
+ self._set_created_by(user_id)
879
+ self._set_updated_at(create_at)
880
+ self._set_updated_by(user_id)
820
881
 
821
882
  if not self._schema.is_schema_node:
822
883
  await self.add_human_friendly_id(db=db)
823
884
  await self.add_display_label(db=db)
824
885
 
825
- query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at)
886
+ query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at, user_id=user_id)
826
887
  await query.execute(db=db)
827
888
 
828
889
  _, self.db_id = query.get_self_ids()
829
890
  self._at = create_at
830
- self._updated_at = create_at
831
891
  self._existing = True
832
892
 
833
893
  new_ids = query.get_ids()
@@ -845,6 +905,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
845
905
  attr: BaseAttribute = getattr(self, name)
846
906
  attr.id, attr.db_id = new_ids[name]
847
907
  attr.at = create_at
908
+ attr._set_created_at(create_at)
909
+ attr._set_created_by(user_id)
910
+ attr._set_updated_at(create_at)
911
+ attr._set_updated_by(user_id)
848
912
  node_changelog.create_attribute(attribute=attr)
849
913
 
850
914
  # Go over the list of relationships and assign the new IDs one by one
@@ -859,7 +923,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
859
923
  return node_changelog
860
924
 
861
925
  async def _update(
862
- self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None
926
+ self, db: InfrahubDatabase, user_id: str, at: Timestamp | None = None, fields: list[str] | None = None
863
927
  ) -> NodeChangelog:
864
928
  """Update the node in the database if needed."""
865
929
 
@@ -869,8 +933,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
869
933
  # Go over the list of Attribute and update them one by one
870
934
  for name in self._attributes:
871
935
  if (fields and name in fields) or not fields:
872
- attr: BaseAttribute = getattr(self, name)
873
- updated_attribute = await attr.save(at=update_at, db=db)
936
+ attr = self.get_attribute(name=name)
937
+ updated_attribute = await attr.save(db=db, user_id=user_id, at=update_at)
874
938
  if updated_attribute:
875
939
  node_changelog.add_attribute(attribute=updated_attribute)
876
940
 
@@ -879,15 +943,15 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
879
943
  for name in self._relationships:
880
944
  if (fields and name in fields) or not fields:
881
945
  processed_relationships.append(name)
882
- rel: RelationshipManager = getattr(self, name)
883
- updated_relationship = await rel.save(at=update_at, db=db)
946
+ rel = self.get_relationship(name=name)
947
+ updated_relationship = await rel.save(db=db, user_id=user_id, at=update_at)
884
948
  node_changelog.add_relationship(relationship_changelog=updated_relationship)
885
949
 
886
950
  if len(processed_relationships) != len(self._relationships):
887
951
  # Analyze if the node has a parent and add it to the changelog if missing
888
952
  if parent_relationship := self._get_parent_relationship_name():
889
953
  if parent_relationship not in processed_relationships:
890
- rel: RelationshipManager = getattr(self, parent_relationship)
954
+ rel = self.get_relationship(name=parent_relationship)
891
955
  if parent := await rel.get_parent(db=db):
892
956
  node_changelog.add_parent_from_relationship(parent=parent)
893
957
 
@@ -915,20 +979,40 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
915
979
  node_changelog.add_attribute(attribute=updated_attribute)
916
980
 
917
981
  node_changelog.display_label = await self.get_display_label(db=db)
982
+
983
+ if node_changelog.has_changes:
984
+ self._set_updated_at(update_at)
985
+ self._set_updated_by(user_id)
986
+ update_branch = self.get_branch_based_on_support_type()
987
+ if update_branch.is_default or update_branch.is_global:
988
+ await self._save_metadata(db=db, branch=update_branch)
918
989
  return node_changelog
919
990
 
920
- async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> Self:
991
+ async def save(
992
+ self,
993
+ db: InfrahubDatabase,
994
+ user_id: str = SYSTEM_USER_ID,
995
+ at: Timestamp | None = None,
996
+ fields: list[str] | None = None,
997
+ ) -> Self:
921
998
  """Create or Update the Node in the database."""
922
999
  save_at = Timestamp(at)
923
1000
 
924
1001
  if self._existing:
925
- self._node_changelog = await self._update(at=save_at, db=db, fields=fields)
1002
+ self._node_changelog = await self._update(db=db, user_id=user_id, at=save_at, fields=fields)
926
1003
  else:
927
- self._node_changelog = await self._create(at=save_at, db=db)
1004
+ self._node_changelog = await self._create(db=db, user_id=user_id, at=save_at)
928
1005
 
929
1006
  return self
930
1007
 
931
- async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
1008
+ async def _save_metadata(self, db: InfrahubDatabase, branch: Branch) -> None:
1009
+ if user_id := self._get_updated_by():
1010
+ update_metadata_query = await NodeUpdateMetadataQuery.init(
1011
+ db=db, branch=branch, node_id=self.get_id(), user_id=user_id, at=self._get_updated_at()
1012
+ )
1013
+ await update_metadata_query.execute(db=db)
1014
+
1015
+ async def delete(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID, at: Timestamp | None = None) -> None:
932
1016
  """Delete the Node in the database."""
933
1017
 
934
1018
  delete_at = Timestamp(at)
@@ -939,25 +1023,32 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
939
1023
  # Go over the list of Attribute and update them one by one
940
1024
  for name in self._attributes:
941
1025
  attr: BaseAttribute = getattr(self, name)
942
- if deleted_attribute := await attr.delete(at=delete_at, db=db):
1026
+ if deleted_attribute := await attr.delete(db=db, at=delete_at, user_id=user_id):
943
1027
  node_changelog.add_attribute(attribute=deleted_attribute)
944
1028
 
945
1029
  if self._human_friendly_id:
946
1030
  if deleted_attribute := await self._human_friendly_id.get_node_attribute(node=self, at=delete_at).delete(
947
- at=delete_at, db=db
1031
+ db=db,
1032
+ user_id=user_id,
1033
+ at=delete_at,
948
1034
  ):
949
1035
  node_changelog.add_attribute(attribute=deleted_attribute)
950
1036
 
951
1037
  if self._display_label:
952
1038
  if deleted_attribute := await self._display_label.get_node_attribute(node=self, at=delete_at).delete(
953
- at=delete_at, db=db
1039
+ db=db, at=delete_at, user_id=user_id
954
1040
  ):
955
1041
  node_changelog.add_attribute(attribute=deleted_attribute)
956
1042
 
957
1043
  branch = self.get_branch_based_on_support_type()
958
1044
 
959
1045
  delete_query = await RelationshipDeleteAllQuery.init(
960
- db=db, node_id=self.get_id(), branch=branch, at=delete_at, branch_agnostic=branch.name == GLOBAL_BRANCH_NAME
1046
+ db=db,
1047
+ node_id=self.get_id(),
1048
+ branch=branch,
1049
+ user_id=user_id,
1050
+ at=delete_at,
1051
+ branch_agnostic=branch.name == GLOBAL_BRANCH_NAME,
961
1052
  )
962
1053
  await delete_query.execute(db=db)
963
1054
 
@@ -965,22 +1056,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
965
1056
  for relationship_changelog in deleted_relationships_changelogs:
966
1057
  node_changelog.add_relationship(relationship_changelog=relationship_changelog)
967
1058
 
968
- # Update the relationship to the branch itself
969
- query = await NodeGetListQuery.init(
970
- db=db,
971
- schema=self._schema,
972
- filters={"id": self.id},
973
- branch=self._branch,
974
- at=delete_at,
975
- order=OrderModel(disable=True),
976
- )
977
- await query.execute(db=db)
978
- result = query.get_result()
979
-
980
- if result and result.get("rb.branch") == branch.name:
981
- await update_relationships_to([result.get("rb_id")], to=delete_at, db=db)
982
-
983
- query = await NodeDeleteQuery.init(db=db, node=self, at=delete_at)
1059
+ query = await NodeDeleteQuery.init(db=db, node=self, at=delete_at, user_id=user_id)
984
1060
  await query.execute(db=db)
985
1061
 
986
1062
  self._node_changelog = node_changelog
@@ -1007,10 +1083,11 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
1007
1083
 
1008
1084
  FIELD_NAME_TO_EXCLUDE = ["id"] + self._schema.relationship_names
1009
1085
 
1010
- if fields and isinstance(fields, dict):
1011
- field_names = [field_name for field_name in fields.keys() if field_name not in FIELD_NAME_TO_EXCLUDE]
1012
- else:
1013
- field_names = self._schema.attribute_names + ["__typename", "display_label"]
1086
+ field_names = (
1087
+ [field_name for field_name in fields.keys() if field_name not in FIELD_NAME_TO_EXCLUDE]
1088
+ if fields and isinstance(fields, dict)
1089
+ else self._schema.attribute_names + ["__typename", "display_label"]
1090
+ )
1014
1091
 
1015
1092
  for field_name in field_names:
1016
1093
  if field_name == "__typename":
@@ -1027,8 +1104,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
1027
1104
  continue
1028
1105
 
1029
1106
  if field_name == "_updated_at":
1030
- if self._updated_at:
1031
- response[field_name] = await self._updated_at.to_graphql()
1107
+ if updated_at := self._get_updated_at():
1108
+ response[field_name] = await updated_at.to_graphql()
1032
1109
  else:
1033
1110
  response[field_name] = None
1034
1111
  continue
@@ -1079,6 +1156,28 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
1079
1156
 
1080
1157
  return response
1081
1158
 
1159
+ async def _build_meta_response(self, field_name: str, fields: dict) -> dict:
1160
+ data = {}
1161
+ for meta_field in fields.get(field_name, {}).keys():
1162
+ if meta_field == "created_at":
1163
+ created_at = self._get_created_at()
1164
+ data["created_at"] = created_at.to_datetime() if created_at else None
1165
+
1166
+ if meta_field == "created_by":
1167
+ data["created_by"] = (
1168
+ {"id": self._get_created_by(), "__kind__": "CoreAccount"} if self._get_created_by() else None
1169
+ )
1170
+
1171
+ if meta_field == "updated_by":
1172
+ data["updated_by"] = (
1173
+ {"id": self._get_updated_by(), "__kind__": "CoreAccount"} if self._get_updated_by() else None
1174
+ )
1175
+
1176
+ if meta_field == "updated_at":
1177
+ updated_at = self._get_updated_at()
1178
+ data["updated_at"] = updated_at.to_datetime() if updated_at else None
1179
+ return data
1180
+
1082
1181
  async def from_graphql(self, data: dict, db: InfrahubDatabase, process_pools: bool = True) -> bool:
1083
1182
  """Update object from a GraphQL payload."""
1084
1183
 
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Any
4
+
3
5
  from ..utils import SubclassWithMeta, SubclassWithMeta_Meta
4
6
 
5
7
 
@@ -9,19 +11,19 @@ class BaseOptions:
9
11
 
10
12
  _frozen: bool = False
11
13
 
12
- def __init__(self, class_type):
14
+ def __init__(self, class_type: type) -> None:
13
15
  self.class_type = class_type
14
16
 
15
17
  def freeze(self) -> None:
16
18
  self._frozen = True
17
19
 
18
- def __setattr__(self, name, value):
20
+ def __setattr__(self, name: str, value: Any) -> None:
19
21
  if not self._frozen:
20
22
  super().__setattr__(name, value)
21
23
  else:
22
24
  raise Exception(f"Can't modify frozen Options {self}")
23
25
 
24
- def __repr__(self):
26
+ def __repr__(self) -> str:
25
27
  return f"<{self.__class__.__name__} name={repr(self.name)}>"
26
28
 
27
29
 
@@ -34,7 +36,9 @@ class BaseNode(SubclassWithMeta):
34
36
  # return type(class_name, (cls,), {"Meta": options})
35
37
 
36
38
  @classmethod
37
- def __init_subclass_with_meta__(cls, name=None, description=None, _meta=None, **_kwargs) -> None:
39
+ def __init_subclass_with_meta__(
40
+ cls, name: str | None = None, description: str | None = None, _meta: BaseOptions | None = None, **_kwargs: Any
41
+ ) -> None:
38
42
  assert "_meta" not in cls.__dict__, "Can't assign meta directly"
39
43
  if not _meta:
40
44
  return
@@ -52,7 +56,7 @@ class BaseNodeOptions(BaseOptions):
52
56
 
53
57
 
54
58
  class ObjectNodeMeta(BaseNodeMeta):
55
- def __new__(mcs, name_, bases, namespace, **options):
59
+ def __new__(mcs, name_: str, bases: tuple[type, ...], namespace: dict[str, Any], **options: Any) -> type:
56
60
  # Note: it's safe to pass options as keyword arguments as they are still type-checked by NodeOptions.
57
61
 
58
62
  # We create this type, to then overload it with the dataclass attrs
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Mapping
4
4
 
5
5
  from infrahub import lock
6
6
  from infrahub.core import registry
7
- from infrahub.core.constants import RelationshipCardinality, RelationshipKind
7
+ from infrahub.core.constants import SYSTEM_USER_ID, RelationshipCardinality, RelationshipKind
8
8
  from infrahub.core.constraint.node.runner import NodeConstraintRunner
9
9
  from infrahub.core.node import Node
10
10
  from infrahub.core.node.lock_utils import get_lock_names_on_object_mutation
@@ -180,11 +180,12 @@ async def _do_create_node(
180
180
  fields_to_validate: list[str],
181
181
  data: dict[str, Any],
182
182
  at: Timestamp | None = None,
183
+ user_id: str = SYSTEM_USER_ID,
183
184
  ) -> Node:
184
185
  obj = await node_class.init(db=db, schema=schema, branch=branch)
185
186
  await obj.new(db=db, **data)
186
187
  await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
187
- await obj.save(db=db, at=at)
188
+ await obj.save(db=db, at=at, user_id=user_id)
188
189
 
189
190
  object_template = await obj.get_object_template(db=db)
190
191
  if object_template:
@@ -205,6 +206,7 @@ async def create_node(
205
206
  branch: Branch,
206
207
  schema: MainSchemaTypes,
207
208
  at: Timestamp | None = None,
209
+ user_id: str = SYSTEM_USER_ID,
208
210
  ) -> Node:
209
211
  """Create a node in the database if constraint checks succeed."""
210
212
 
@@ -237,6 +239,7 @@ async def create_node(
237
239
  fields_to_validate=fields_to_validate,
238
240
  data=data,
239
241
  at=at,
242
+ user_id=user_id,
240
243
  )
241
244
  else:
242
245
  async with db.start_transaction() as dbt:
@@ -253,11 +256,12 @@ async def create_node(
253
256
  fields_to_validate=fields_to_validate,
254
257
  data=data,
255
258
  at=at,
259
+ user_id=user_id,
256
260
  )
257
261
 
258
262
  if await get_profile_ids(db=db, obj=obj):
259
263
  node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
260
264
  await node_profiles_applier.apply_profiles(node=obj)
261
- await obj.save(db=db)
265
+ await obj.save(db=db, user_id=user_id)
262
266
 
263
267
  return obj
@@ -117,7 +117,7 @@ class NodeDeleteIndex:
117
117
 
118
118
 
119
119
  class NodeDeleteValidator:
120
- def __init__(self, db: InfrahubDatabase, branch: Branch):
120
+ def __init__(self, db: InfrahubDatabase, branch: Branch) -> None:
121
121
  self.db = db
122
122
  self.branch = branch
123
123
  schema_branch = registry.schema.get_schema_branch(name=self.branch.name)