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
@@ -15,18 +15,27 @@ from pydantic import BaseModel, Field
15
15
  from infrahub import config
16
16
  from infrahub.core import registry
17
17
  from infrahub.core.changelog.models import AttributeChangelog
18
- from infrahub.core.constants import NULL_VALUE, AttributeDBNodeType, BranchSupportType, RelationshipStatus
18
+ from infrahub.core.constants import (
19
+ NULL_VALUE,
20
+ SYSTEM_USER_ID,
21
+ AttributeDBNodeType,
22
+ BranchSupportType,
23
+ InfrahubKind,
24
+ MetadataOptions,
25
+ )
26
+ from infrahub.core.metadata.interface import MetadataInterface
27
+ from infrahub.core.metadata.model import MetadataInfo
19
28
  from infrahub.core.property import FlagPropertyMixin, NodePropertyData, NodePropertyMixin
20
29
  from infrahub.core.query.attribute import (
21
30
  AttributeClearNodePropertyQuery,
22
- AttributeGetQuery,
31
+ AttributeDeleteQuery,
23
32
  AttributeUpdateFlagQuery,
24
33
  AttributeUpdateNodePropertyQuery,
25
34
  AttributeUpdateValueQuery,
26
35
  )
27
36
  from infrahub.core.query.node import AttributeFromDB, NodeListGetAttributeQuery
28
37
  from infrahub.core.timestamp import Timestamp
29
- from infrahub.core.utils import add_relationship, convert_ip_to_binary_str, update_relationships_to
38
+ from infrahub.core.utils import convert_ip_to_binary_str
30
39
  from infrahub.exceptions import ValidationError
31
40
  from infrahub.helpers import hash_password
32
41
 
@@ -70,12 +79,11 @@ class AttributeCreateData(BaseModel):
70
79
  content: dict[str, Any]
71
80
  is_default: bool
72
81
  is_protected: bool
73
- is_visible: bool
74
82
  source_prop: list[NodePropertyData] = Field(default_factory=list)
75
83
  owner_prop: list[NodePropertyData] = Field(default_factory=list)
76
84
 
77
85
 
78
- class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
86
+ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin, MetadataInterface):
79
87
  type: type | tuple[type] | None = None
80
88
 
81
89
  _rel_to_node_label: str = RELATIONSHIP_TO_NODE_LABEL
@@ -91,7 +99,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
91
99
  id: str | None = None,
92
100
  db_id: str | None = None,
93
101
  data: dict | str | AttributeFromDB | None = None,
94
- updated_at: Timestamp | str | None = None,
95
102
  is_default: bool = False,
96
103
  is_from_profile: bool = False,
97
104
  **kwargs: dict[str, Any],
@@ -99,7 +106,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
99
106
  self.id = id
100
107
  self.db_id = db_id
101
108
 
102
- self.updated_at = updated_at
103
109
  self.name = name
104
110
  self.node = node
105
111
  self.schema = schema
@@ -112,6 +118,13 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
112
118
  self._init_node_property_mixin(kwargs)
113
119
  self._init_flag_property_mixin(kwargs)
114
120
 
121
+ self._metadata = MetadataInfo(
122
+ created_at=kwargs.get("created_at"),
123
+ created_by=kwargs.get("created_by"),
124
+ updated_at=kwargs.get("updated_at"),
125
+ updated_by=kwargs.get("updated_by"),
126
+ )
127
+
115
128
  self.value = None
116
129
 
117
130
  if isinstance(data, AttributeFromDB):
@@ -130,8 +143,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
130
143
  for field_name in fields_to_extract_from_data:
131
144
  setattr(self, field_name, data.get(field_name, None))
132
145
 
133
- if not self.updated_at and "updated_at" in data:
134
- self.updated_at = Timestamp(data.get("updated_at"))
135
146
  elif data is None:
136
147
  self.is_default = True
137
148
  else:
@@ -151,9 +162,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
151
162
  if self.is_protected is None:
152
163
  self.is_protected = False
153
164
 
154
- if self.is_visible is None:
155
- self.is_visible = True
156
-
157
165
  @property
158
166
  def is_enum(self) -> bool:
159
167
  return bool(self.schema.enum)
@@ -187,6 +195,30 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
187
195
  if self.is_enum and self.value:
188
196
  self.value = self.schema.convert_value_to_enum(self.value)
189
197
 
198
+ def _set_created_at(self, value: Timestamp | None) -> None:
199
+ self._metadata.created_at = value
200
+
201
+ def _set_created_by(self, value: str | None) -> None:
202
+ self._metadata.created_by = value
203
+
204
+ def _set_updated_at(self, value: Timestamp | None) -> None:
205
+ self._metadata.updated_at = value
206
+
207
+ def _set_updated_by(self, value: str | None) -> None:
208
+ self._metadata.updated_by = value
209
+
210
+ def _get_created_at(self) -> Timestamp | None:
211
+ return self._metadata.created_at
212
+
213
+ def _get_created_by(self) -> str | None:
214
+ return self._metadata.created_by
215
+
216
+ def _get_updated_at(self) -> Timestamp | None:
217
+ return self._metadata.updated_at
218
+
219
+ def _get_updated_by(self) -> str | None:
220
+ return self._metadata.updated_by
221
+
190
222
  @staticmethod
191
223
  def get_allowed_property_in_path() -> list[str]:
192
224
  return ["value"]
@@ -302,8 +334,10 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
302
334
  if prop_name in data.node_properties:
303
335
  setattr(self, prop_name, data.node_properties[prop_name].uuid)
304
336
 
305
- if not self.updated_at and data.updated_at:
306
- self.updated_at = Timestamp(data.updated_at)
337
+ self._set_created_at(data.created_at)
338
+ self._set_created_by(data.created_by)
339
+ self._set_updated_at(data.updated_at)
340
+ self._set_updated_by(data.updated_by)
307
341
 
308
342
  def value_from_db(self, data: AttributeFromDB) -> Any:
309
343
  if data.value == NULL_VALUE:
@@ -320,7 +354,9 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
320
354
  """Deserialize the value coming from the database."""
321
355
  return data.value
322
356
 
323
- async def save(self, db: InfrahubDatabase, at: Timestamp | None = None) -> AttributeChangelog | None:
357
+ async def save(
358
+ self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID, at: Timestamp | None = None
359
+ ) -> AttributeChangelog | None:
324
360
  """Create or Update the Attribute in the database."""
325
361
 
326
362
  save_at = Timestamp(at)
@@ -328,70 +364,34 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
328
364
  if not self.id:
329
365
  return None
330
366
 
331
- return await self._update(at=save_at, db=db)
367
+ return await self._update(db=db, user_id=user_id, at=save_at)
332
368
 
333
- async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> AttributeChangelog | None:
369
+ async def delete(
370
+ self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID, at: Timestamp | None = None
371
+ ) -> AttributeChangelog | None:
334
372
  if not self.db_id:
335
373
  return None
336
374
 
337
375
  delete_at = Timestamp(at)
376
+ branch = self.get_branch_based_on_support_type()
338
377
 
339
- query = await AttributeGetQuery.init(db=db, attr=self, at=delete_at)
378
+ query = await AttributeDeleteQuery.init(db=db, branch=branch, attr=self, user_id=user_id, at=delete_at)
340
379
  await query.execute(db=db)
341
- results = query.get_results()
380
+ previous_value = query.get_previous_property_value()
342
381
 
343
- if not results:
382
+ if not previous_value:
344
383
  return None
345
384
 
346
- changelog = AttributeChangelog(
385
+ return AttributeChangelog(
347
386
  name=self.name,
348
387
  value=None,
349
- value_previous=None,
388
+ value_previous=previous_value,
350
389
  kind=self.schema.kind,
351
390
  )
352
391
 
353
- properties_to_delete = []
354
- branch = self.get_branch_based_on_support_type()
355
-
356
- # Check all the relationship and update the one that are in the same branch
357
- rel_ids_to_update = set()
358
- for result in results:
359
- if result.get_rel("r2").type == "HAS_VALUE":
360
- changelog.value_previous = result.get_node("ap").get("value")
361
- properties_to_delete.append((result.get_rel("r2").type, result.get_node("ap").element_id))
362
-
363
- await add_relationship(
364
- src_node_id=self.db_id,
365
- dst_node_id=result.get_node("ap").element_id,
366
- rel_type=result.get_rel("r2").type,
367
- branch_name=branch.name,
368
- branch_level=branch.hierarchy_level,
369
- at=delete_at,
370
- status=RelationshipStatus.DELETED,
371
- db=db,
372
- )
373
-
374
- for rel in result.get_rels():
375
- if rel.get("branch") == branch.name:
376
- rel_ids_to_update.add(rel.element_id)
377
-
378
- if rel_ids_to_update:
379
- await update_relationships_to(ids=list(rel_ids_to_update), to=delete_at, db=db)
380
-
381
- await add_relationship(
382
- src_node_id=self.node.db_id,
383
- dst_node_id=self.db_id,
384
- rel_type="HAS_ATTRIBUTE",
385
- branch_name=branch.name,
386
- branch_level=branch.hierarchy_level,
387
- at=delete_at,
388
- status=RelationshipStatus.DELETED,
389
- db=db,
390
- )
391
-
392
- return changelog
393
-
394
- async def _update(self, db: InfrahubDatabase, at: Timestamp | None = None) -> AttributeChangelog | None:
392
+ async def _update(
393
+ self, db: InfrahubDatabase, user_id: str, at: Timestamp | None = None
394
+ ) -> AttributeChangelog | None:
395
395
  """Update the attribute in the database.
396
396
 
397
397
  Get the current value
@@ -422,13 +422,10 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
422
422
  fields={self.name: True},
423
423
  branch=self.branch,
424
424
  at=update_at,
425
- include_source=True,
426
- include_owner=True,
425
+ include_metadata=MetadataOptions.LINKED_NODES,
427
426
  )
428
427
  await query.execute(db=db)
429
- current_attr_data, current_attr_result = query.get_result_by_id_and_name(self.node.id, self.name)
430
-
431
- branch = self.get_branch_based_on_support_type()
428
+ current_attr_data, _ = query.get_result_by_id_and_name(self.node.id, self.name)
432
429
 
433
430
  changelog = AttributeChangelog(
434
431
  name=self.name,
@@ -437,36 +434,26 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
437
434
  kind=self.schema.kind,
438
435
  )
439
436
 
437
+ branch = self.get_branch_based_on_support_type()
438
+
440
439
  # ---------- Update the Value ----------
441
440
  if current_attr_data.content != self.to_db():
442
441
  # Create the new AttributeValue and update the existing relationship
443
- query = await AttributeUpdateValueQuery.init(db=db, attr=self, at=update_at)
442
+ query = await AttributeUpdateValueQuery.init(db=db, branch=branch, attr=self, user_id=user_id, at=update_at)
444
443
  await query.execute(db=db)
445
444
 
446
- # TODO check that everything went well
447
- rel = current_attr_result.get_rel("r2")
448
- if rel.get("branch") == branch.name:
449
- await update_relationships_to([rel.element_id], to=update_at, db=db)
450
-
451
445
  # ---------- Update the Flags ----------
452
- SUPPORTED_FLAGS = (
453
- ("is_visible", "isv", "rel_isv"),
454
- ("is_protected", "isp", "rel_isp"),
455
- )
456
-
457
- for flag_name, _, rel_name in SUPPORTED_FLAGS:
458
- if current_attr_data.flag_properties[flag_name] != getattr(self, flag_name):
459
- changelog.add_property(
460
- name=flag_name,
461
- value_current=getattr(self, flag_name),
462
- value_previous=current_attr_data.flag_properties[flag_name],
463
- )
464
- query = await AttributeUpdateFlagQuery.init(db=db, attr=self, at=update_at, flag_name=flag_name)
465
- await query.execute(db=db)
466
-
467
- rel = current_attr_result.get(rel_name)
468
- if rel.get("branch") == branch.name:
469
- await update_relationships_to([rel.element_id], to=update_at, db=db)
446
+ flag_name = "is_protected"
447
+ if current_attr_data.flag_properties[flag_name] != getattr(self, flag_name):
448
+ changelog.add_property(
449
+ name=flag_name,
450
+ value_current=getattr(self, flag_name),
451
+ value_previous=current_attr_data.flag_properties[flag_name],
452
+ )
453
+ query = await AttributeUpdateFlagQuery.init(
454
+ db=db, branch=branch, attr=self, user_id=user_id, at=update_at, flag_name=flag_name
455
+ )
456
+ await query.execute(db=db)
470
457
 
471
458
  # ---------- Update the Node Properties ----------
472
459
  for prop_name in self._node_properties:
@@ -488,21 +475,27 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
488
475
 
489
476
  if needs_update:
490
477
  query = await AttributeUpdateNodePropertyQuery.init(
491
- db=db, attr=self, at=update_at, prop_name=prop_name, prop_id=current_prop_id
478
+ db=db,
479
+ branch=branch,
480
+ attr=self,
481
+ user_id=user_id,
482
+ at=update_at,
483
+ prop_name=prop_name,
484
+ prop_id=current_prop_id,
492
485
  )
493
486
  await query.execute(db=db)
494
487
 
495
488
  if needs_clear:
496
489
  query = await AttributeClearNodePropertyQuery.init(
497
- db=db, attr=self, at=update_at, prop_name=prop_name, prop_id=database_prop_id
490
+ db=db,
491
+ branch=branch,
492
+ attr=self,
493
+ user_id=user_id,
494
+ at=update_at,
495
+ prop_name=prop_name,
498
496
  )
499
497
  await query.execute(db=db)
500
498
 
501
- # set the to time on the previously active edge
502
- rel = current_attr_result.get(f"rel_{prop_name}")
503
- if rel and rel.get("branch") == branch.name:
504
- await update_relationships_to([rel.element_id], to=update_at, db=db)
505
-
506
499
  if changelog.has_updates:
507
500
  return changelog
508
501
 
@@ -531,10 +524,19 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
531
524
 
532
525
  for field_name in field_names:
533
526
  if field_name == "updated_at":
534
- if self.updated_at:
535
- response[field_name] = await self.updated_at.to_graphql()
536
- else:
537
- response[field_name] = None
527
+ updated_at = self._get_updated_at()
528
+ response[field_name] = await updated_at.to_graphql() if updated_at else None
529
+ continue
530
+
531
+ if field_name == "updated_by":
532
+ response[field_name] = (
533
+ {
534
+ "id": self._get_updated_by(),
535
+ "__kind__": InfrahubKind.ACCOUNT,
536
+ }
537
+ if self._get_updated_by()
538
+ else None
539
+ )
538
540
  continue
539
541
 
540
542
  if field_name == "__typename":
@@ -620,9 +622,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
620
622
  if "is_protected" in data and data["is_protected"] != self.is_protected:
621
623
  self.is_protected = data["is_protected"]
622
624
  changed = True
623
- if "is_visible" in data and data["is_visible"] != self.is_visible:
624
- self.is_visible = data["is_visible"]
625
- changed = True
626
625
 
627
626
  if "source" in data and data["source"] != self.source_id:
628
627
  self.source = data["source"]
@@ -657,7 +656,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
657
656
  content=self.to_db(),
658
657
  is_default=self.is_default,
659
658
  is_protected=self.is_protected,
660
- is_visible=self.is_visible,
661
659
  )
662
660
  if self.source_id:
663
661
  data.source_prop.append(NodePropertyData(name="source", peer_id=self.source_id))
@@ -6,17 +6,15 @@ from typing import TYPE_CHECKING, Any, Optional, Self, Union, cast
6
6
  from pydantic import Field, field_validator
7
7
 
8
8
  from infrahub.core.branch.enums import BranchStatus
9
- from infrahub.core.constants import GLOBAL_BRANCH_NAME
9
+ from infrahub.core.constants import GLOBAL_BRANCH_NAME, SYSTEM_USER_ID
10
10
  from infrahub.core.graph import GRAPH_VERSION
11
11
  from infrahub.core.models import SchemaBranchHash # noqa: TC001
12
- from infrahub.core.node.standard import StandardNode
12
+ from infrahub.core.node.standard import StandardNode, StandardNodeOrdering
13
13
  from infrahub.core.query import Query, QueryType
14
14
  from infrahub.core.query.branch import (
15
15
  BranchNodeGetListQuery,
16
16
  DeleteBranchRelationshipsQuery,
17
- GetAllBranchInternalRelationshipQuery,
18
- RebaseBranchDeleteRelationshipQuery,
19
- RebaseBranchUpdateRelationshipQuery,
17
+ RebaseBranchQuery,
20
18
  )
21
19
  from infrahub.core.registry import registry
22
20
  from infrahub.core.timestamp import Timestamp
@@ -37,7 +35,6 @@ class Branch(StandardNode):
37
35
  origin_branch: str = "main"
38
36
  branched_from: Optional[str] = Field(default=None, validate_default=True)
39
37
  hierarchy_level: int = 2
40
- created_at: Optional[str] = Field(default=None, validate_default=True)
41
38
  is_default: bool = False
42
39
  is_global: bool = False
43
40
  is_protected: bool = False
@@ -93,11 +90,6 @@ class Branch(StandardNode):
93
90
  raise RuntimeError(f"branched_from not set for branch {self.name}")
94
91
  return self.branched_from
95
92
 
96
- @field_validator("created_at", mode="before")
97
- @classmethod
98
- def set_created_at(cls, value: str) -> str:
99
- return Timestamp(value).to_string()
100
-
101
93
  def get_created_at(self) -> str:
102
94
  if not self.created_at:
103
95
  raise RuntimeError(f"created_at not set for branch {self.name}")
@@ -162,10 +154,18 @@ class Branch(StandardNode):
162
154
  limit: int = 1000,
163
155
  ids: list[str] | None = None,
164
156
  name: str | None = None,
157
+ node_ordering: StandardNodeOrdering | None = None,
165
158
  **kwargs: Any,
166
159
  ) -> list[Self]:
160
+ node_ordering = node_ordering or StandardNodeOrdering()
167
161
  query: Query = await BranchNodeGetListQuery.init(
168
- db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs
162
+ db=db,
163
+ node_class=cls,
164
+ ids=ids,
165
+ node_name=name,
166
+ limit=limit,
167
+ node_ordering=node_ordering,
168
+ **kwargs,
169
169
  )
170
170
  await query.execute(db=db)
171
171
 
@@ -178,10 +178,18 @@ class Branch(StandardNode):
178
178
  limit: int = 1000,
179
179
  ids: list[str] | None = None,
180
180
  name: str | None = None,
181
+ partial_match: bool = False,
181
182
  **kwargs: Any,
182
183
  ) -> int:
183
184
  query: Query = await BranchNodeGetListQuery.init(
184
- db=db, node_class=cls, ids=ids, node_name=name, limit=limit, exclude_global=True, **kwargs
185
+ db=db,
186
+ node_class=cls,
187
+ ids=ids,
188
+ node_name=name,
189
+ limit=limit,
190
+ exclude_global=True,
191
+ partial_match=partial_match,
192
+ **kwargs,
185
193
  )
186
194
  return await query.count(db=db)
187
195
 
@@ -208,7 +216,7 @@ class Branch(StandardNode):
208
216
 
209
217
  return [default_branch, self.name]
210
218
 
211
- def get_branches_and_times_to_query(self, at: Optional[Union[Timestamp, str]] = None) -> dict[frozenset, str]:
219
+ def get_branches_and_times_to_query(self, at: Optional[Timestamp] = None) -> dict[frozenset, str]:
212
220
  """Return all the names of the branches that are constituing this branch with the associated times excluding the global branch"""
213
221
 
214
222
  at = Timestamp(at)
@@ -229,7 +237,7 @@ class Branch(StandardNode):
229
237
 
230
238
  def get_branches_and_times_to_query_global(
231
239
  self,
232
- at: Optional[Union[Timestamp, str]] = None,
240
+ at: Optional[Timestamp] = None,
233
241
  is_isolated: bool = True,
234
242
  ) -> dict[frozenset, str]:
235
243
  """Return all the names of the branches that are constituting this branch with the associated times."""
@@ -281,9 +289,9 @@ class Branch(StandardNode):
281
289
 
282
290
  return start, end
283
291
 
284
- async def create(self, db: InfrahubDatabase) -> bool:
292
+ async def create(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> bool:
285
293
  self.graph_version = GRAPH_VERSION
286
- return await super().create(db=db)
294
+ return await super().create(db=db, user_id=user_id)
287
295
 
288
296
  async def delete(self, db: InfrahubDatabase) -> None:
289
297
  if self.is_default:
@@ -299,7 +307,7 @@ class Branch(StandardNode):
299
307
  await super().delete(db=db)
300
308
 
301
309
  def get_query_filter_relationships(
302
- self, rel_labels: list, at: Optional[Union[Timestamp, str]] = None, include_outside_parentheses: bool = False
310
+ self, rel_labels: list, at: Optional[Timestamp] = None, include_outside_parentheses: bool = False
303
311
  ) -> tuple[list, dict]:
304
312
  """
305
313
  Generate a CYPHER Query filter based on a list of relationships to query a part of the graph at a specific time and on a specific branch.
@@ -362,7 +370,7 @@ class Branch(StandardNode):
362
370
  params[f"{pp}time1"] = at_str
363
371
  return filter_str, params
364
372
 
365
- branches_times = self.get_branches_and_times_to_query_global(at=at_str, is_isolated=is_isolated)
373
+ branches_times = self.get_branches_and_times_to_query_global(at=at, is_isolated=is_isolated)
366
374
 
367
375
  for idx, (branch_name, time_to_query) in enumerate(branches_times.items()):
368
376
  params[f"{pp}branch{idx}"] = list(branch_name)
@@ -387,8 +395,8 @@ class Branch(StandardNode):
387
395
  def get_query_filter_relationships_range(
388
396
  self,
389
397
  rel_labels: list,
390
- start_time: Union[Timestamp, str],
391
- end_time: Union[Timestamp, str],
398
+ start_time: Timestamp,
399
+ end_time: Timestamp,
392
400
  include_outside_parentheses: bool = False,
393
401
  include_global: bool = False,
394
402
  ) -> tuple[list, dict]:
@@ -468,9 +476,7 @@ class Branch(StandardNode):
468
476
 
469
477
  return filters, params
470
478
 
471
- def get_query_filter_range(
472
- self, rel_label: list, start_time: Union[Timestamp, str], end_time: Union[Timestamp, str]
473
- ) -> tuple[list, dict]:
479
+ def get_query_filter_range(self, rel_label: list, start_time: Timestamp, end_time: Timestamp) -> tuple[list, dict]:
474
480
  """
475
481
  Generate a CYPHER Query filter to query a range of values in the graph between start_time and end_time."""
476
482
 
@@ -496,67 +502,34 @@ class Branch(StandardNode):
496
502
 
497
503
  return filters, params
498
504
 
499
- async def rebase(self, db: InfrahubDatabase, at: Optional[Union[str, Timestamp]] = None) -> None:
505
+ async def rebase(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> None:
500
506
  """Rebase the current Branch with its origin branch"""
501
507
 
502
- at = Timestamp(at)
503
-
504
- # Find all relationships with the name of the branch
505
- # Delete all relationship that have a to date defined in the past
506
- # Update the from time on all other relationships
507
- # If conflict is set, ignore the one with Drop
508
+ at = Timestamp()
508
509
 
509
510
  await self.rebase_graph(db=db, at=at)
510
511
 
511
- # FIXME, we must ensure that there is no conflict before rebasing a branch
512
- # Otherwise we could endup with a complicated situation
513
512
  self.branched_from = at.to_string()
514
513
  self.status = BranchStatus.OPEN
515
- await self.save(db=db)
514
+ await self.save(db=db, user_id=user_id)
516
515
 
517
516
  # Update the branch in the registry after the rebase
518
517
  registry.branch[self.name] = self
519
518
 
520
- async def rebase_graph(self, db: InfrahubDatabase, at: Optional[Timestamp] = None) -> None:
521
- at = Timestamp(at)
519
+ async def rebase_graph(self, db: InfrahubDatabase, at: Timestamp) -> None:
520
+ """Rebase all relationships on this branch to a new point in time.
522
521
 
523
- query = await GetAllBranchInternalRelationshipQuery.init(db=db, branch=self)
524
- await query.execute(db=db)
522
+ This method updates the graph to reflect the state of the branch as if it had been created
523
+ at the specified timestamp. Relationships are processed as follows:
525
524
 
526
- rels_to_delete = []
527
- rels_to_update = []
528
- for result in query.get_results():
529
- element_id = result.get("r").element_id
525
+ - Relationships with no `to` timestamp and `from` <= at: Updated to start from `at`
526
+ - Relationships with `to` < at: Deleted (ended before rebase point)
527
+ - Relationships with `to` >= at: Updated to start from `at`
530
528
 
531
- conflict_status = result.get("r").get("conflict", None)
532
- if conflict_status and conflict_status == "drop":
533
- rels_to_delete.append(element_id)
534
- continue
535
-
536
- time_to_str = result.get("r").get("to", None)
537
- time_from_str = result.get("r").get("from")
538
- time_from = Timestamp(time_from_str)
539
-
540
- if not time_to_str and time_from_str and time_from <= at:
541
- rels_to_update.append(element_id)
542
- continue
543
-
544
- if not time_to_str and time_from_str and time_from > at:
545
- rels_to_delete.append(element_id)
546
- continue
547
-
548
- time_to = Timestamp(time_to_str)
549
- if time_to < at:
550
- rels_to_delete.append(element_id)
551
- continue
552
-
553
- rels_to_update.append(element_id)
554
-
555
- update_query = await RebaseBranchUpdateRelationshipQuery.init(db=db, ids=rels_to_update, at=at)
556
- await update_query.execute(db=db)
557
-
558
- delete_query = await RebaseBranchDeleteRelationshipQuery.init(db=db, ids=rels_to_delete, at=at)
559
- await delete_query.execute(db=db)
529
+ Orphaned nodes (nodes with no remaining relationships) are also cleaned up.
530
+ """
531
+ query = await RebaseBranchQuery.init(db=db, branch=self, at=at)
532
+ await query.execute(db=db)
560
533
 
561
534
 
562
535
  registry.branch_object = Branch
@@ -170,7 +170,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
170
170
  migrations = []
171
171
  async with lock.registry.global_graph_lock():
172
172
  async with db.start_transaction() as dbt:
173
- await obj.rebase(db=dbt)
173
+ await obj.rebase(db=dbt, user_id=context.account.account_id)
174
174
  log.info("Branch successfully rebased")
175
175
 
176
176
  if obj.has_schema_changes:
@@ -187,7 +187,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
187
187
  )
188
188
  registry.schema.set_schema_branch(name=obj.name, schema=updated_schema)
189
189
  obj.update_schema_hash()
190
- await obj.save(db=db)
190
+ await obj.save(db=db, user_id=context.account.account_id)
191
191
 
192
192
  # Execute the migrations
193
193
  migrations = await merger.calculate_migrations(target_schema=updated_schema)
@@ -198,6 +198,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
198
198
  new_schema=candidate_schema,
199
199
  previous_schema=schema_in_main_before,
200
200
  migrations=migrations,
201
+ user_id=context.account.account_id,
201
202
  )
202
203
  )
203
204
  for error in errors:
@@ -302,6 +303,7 @@ async def merge_branch(branch: str, context: InfrahubContext, proposed_change_id
302
303
  new_schema=merger.destination_schema,
303
304
  previous_schema=merger.initial_source_schema,
304
305
  migrations=merger.migrations,
306
+ user_id=context.account.account_id,
305
307
  )
306
308
  )
307
309
  for error in errors:
@@ -431,7 +433,7 @@ async def create_branch(model: BranchCreateModel, context: InfrahubContext) -> N
431
433
  new_schema = origin_schema.duplicate(name=obj.name)
432
434
  registry.schema.set_schema_branch(name=obj.name, schema=new_schema)
433
435
  obj.update_schema_hash()
434
- await obj.save(db=db)
436
+ await obj.save(db=db, user_id=context.account.account_id)
435
437
 
436
438
  # Add Branch to registry
437
439
  registry.branch[obj.name] = obj