infrahub-server 1.4.9__py3-none-any.whl → 1.5.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 (103) hide show
  1. infrahub/actions/tasks.py +200 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/query.py +2 -0
  4. infrahub/api/schema.py +3 -0
  5. infrahub/auth.py +5 -5
  6. infrahub/cli/db.py +2 -2
  7. infrahub/config.py +7 -2
  8. infrahub/core/attribute.py +22 -19
  9. infrahub/core/branch/models.py +2 -2
  10. infrahub/core/branch/needs_rebase_status.py +11 -0
  11. infrahub/core/branch/tasks.py +2 -2
  12. infrahub/core/constants/__init__.py +1 -0
  13. infrahub/core/convert_object_type/object_conversion.py +201 -0
  14. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  15. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  16. infrahub/core/diff/query/artifact.py +12 -9
  17. infrahub/core/graph/__init__.py +1 -1
  18. infrahub/core/initialization.py +2 -2
  19. infrahub/core/manager.py +3 -81
  20. infrahub/core/migrations/graph/__init__.py +2 -0
  21. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  22. infrahub/core/node/__init__.py +26 -3
  23. infrahub/core/node/create.py +79 -38
  24. infrahub/core/node/lock_utils.py +98 -0
  25. infrahub/core/property.py +11 -0
  26. infrahub/core/protocols.py +1 -0
  27. infrahub/core/query/attribute.py +27 -15
  28. infrahub/core/query/node.py +47 -184
  29. infrahub/core/query/relationship.py +43 -26
  30. infrahub/core/query/subquery.py +0 -8
  31. infrahub/core/relationship/model.py +59 -19
  32. infrahub/core/schema/attribute_schema.py +0 -2
  33. infrahub/core/schema/definitions/core/repository.py +7 -0
  34. infrahub/core/schema/relationship_schema.py +0 -1
  35. infrahub/core/schema/schema_branch.py +3 -2
  36. infrahub/generators/models.py +31 -12
  37. infrahub/generators/tasks.py +3 -1
  38. infrahub/git/base.py +38 -1
  39. infrahub/graphql/api/dependencies.py +2 -4
  40. infrahub/graphql/api/endpoints.py +2 -2
  41. infrahub/graphql/app.py +2 -4
  42. infrahub/graphql/initialization.py +2 -3
  43. infrahub/graphql/manager.py +212 -137
  44. infrahub/graphql/middleware.py +12 -0
  45. infrahub/graphql/mutations/branch.py +11 -0
  46. infrahub/graphql/mutations/computed_attribute.py +110 -3
  47. infrahub/graphql/mutations/convert_object_type.py +34 -13
  48. infrahub/graphql/mutations/ipam.py +21 -8
  49. infrahub/graphql/mutations/main.py +37 -153
  50. infrahub/graphql/mutations/profile.py +195 -0
  51. infrahub/graphql/mutations/proposed_change.py +2 -1
  52. infrahub/graphql/mutations/repository.py +22 -83
  53. infrahub/graphql/mutations/webhook.py +1 -1
  54. infrahub/graphql/registry.py +173 -0
  55. infrahub/graphql/schema.py +4 -1
  56. infrahub/lock.py +52 -26
  57. infrahub/locks/__init__.py +0 -0
  58. infrahub/locks/tasks.py +37 -0
  59. infrahub/patch/plan_writer.py +2 -2
  60. infrahub/profiles/__init__.py +0 -0
  61. infrahub/profiles/node_applier.py +101 -0
  62. infrahub/profiles/queries/__init__.py +0 -0
  63. infrahub/profiles/queries/get_profile_data.py +99 -0
  64. infrahub/profiles/tasks.py +63 -0
  65. infrahub/repositories/__init__.py +0 -0
  66. infrahub/repositories/create_repository.py +113 -0
  67. infrahub/tasks/registry.py +6 -4
  68. infrahub/webhook/models.py +1 -1
  69. infrahub/workflows/catalogue.py +38 -3
  70. infrahub/workflows/models.py +17 -2
  71. infrahub_sdk/branch.py +5 -8
  72. infrahub_sdk/client.py +364 -84
  73. infrahub_sdk/convert_object_type.py +61 -0
  74. infrahub_sdk/ctl/check.py +2 -3
  75. infrahub_sdk/ctl/cli_commands.py +16 -12
  76. infrahub_sdk/ctl/config.py +8 -2
  77. infrahub_sdk/ctl/generator.py +2 -3
  78. infrahub_sdk/ctl/repository.py +39 -1
  79. infrahub_sdk/ctl/schema.py +12 -1
  80. infrahub_sdk/ctl/utils.py +4 -0
  81. infrahub_sdk/ctl/validate.py +5 -3
  82. infrahub_sdk/diff.py +4 -5
  83. infrahub_sdk/exceptions.py +2 -0
  84. infrahub_sdk/graphql.py +7 -2
  85. infrahub_sdk/node/attribute.py +2 -0
  86. infrahub_sdk/node/node.py +28 -20
  87. infrahub_sdk/playback.py +1 -2
  88. infrahub_sdk/protocols.py +40 -6
  89. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  90. infrahub_sdk/pytest_plugin/utils.py +40 -0
  91. infrahub_sdk/repository.py +1 -2
  92. infrahub_sdk/schema/main.py +1 -0
  93. infrahub_sdk/spec/object.py +43 -4
  94. infrahub_sdk/spec/range_expansion.py +118 -0
  95. infrahub_sdk/timestamp.py +18 -6
  96. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/METADATA +20 -24
  97. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/RECORD +102 -84
  98. infrahub_testcontainers/models.py +2 -2
  99. infrahub_testcontainers/performance_test.py +4 -4
  100. infrahub/core/convert_object_type/conversion.py +0 -134
  101. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/LICENSE.txt +0 -0
  102. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/WHEEL +0 -0
  103. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/entry_points.txt +0 -0
infrahub/actions/tasks.py CHANGED
@@ -1,17 +1,108 @@
1
1
  from __future__ import annotations
2
2
 
3
- from infrahub_sdk.graphql import Mutation
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from infrahub_sdk.graphql import Mutation, Query
7
+ from infrahub_sdk.types import Order
4
8
  from prefect import flow
5
9
 
6
10
  from infrahub.context import InfrahubContext # noqa: TC001 needed for prefect flow
11
+ from infrahub.core.constants import InfrahubKind
12
+ from infrahub.generators.models import (
13
+ GeneratorDefinitionModel,
14
+ RequestGeneratorRun,
15
+ )
7
16
  from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
8
17
  from infrahub.trigger.models import TriggerType
9
18
  from infrahub.trigger.setup import setup_triggers_specific
19
+ from infrahub.workers.dependencies import get_client, get_workflow
20
+ from infrahub.workflows.catalogue import REQUEST_GENERATOR_RUN
10
21
  from infrahub.workflows.utils import add_tags
11
22
 
12
23
  from .gather import gather_trigger_action_rules
13
24
  from .models import EventGroupMember # noqa: TC001 needed for prefect flow
14
25
 
26
+ if TYPE_CHECKING:
27
+ from infrahub_sdk.client import InfrahubClient
28
+ from infrahub_sdk.node import InfrahubNode
29
+
30
+
31
+ def get_generator_run_query(definition_id: str, target_ids: list[str]) -> Query:
32
+ return Query(
33
+ name=InfrahubKind.GENERATORDEFINITION,
34
+ query={
35
+ InfrahubKind.GENERATORDEFINITION: {
36
+ "@filters": {
37
+ "ids": [definition_id],
38
+ },
39
+ "edges": {
40
+ "node": {
41
+ "id": None,
42
+ "name": {
43
+ "value": None,
44
+ },
45
+ "class_name": {
46
+ "value": None,
47
+ },
48
+ "file_path": {
49
+ "value": None,
50
+ },
51
+ "query": {
52
+ "node": {
53
+ "name": {
54
+ "value": None,
55
+ },
56
+ },
57
+ },
58
+ "convert_query_response": {
59
+ "value": None,
60
+ },
61
+ "parameters": {
62
+ "value": None,
63
+ },
64
+ "targets": {
65
+ "node": {
66
+ "id": None,
67
+ "members": {
68
+ "@filters": {
69
+ "ids": target_ids,
70
+ },
71
+ "edges": {
72
+ "node": {
73
+ "__typename": None,
74
+ "id": None,
75
+ "display_label": None,
76
+ },
77
+ },
78
+ },
79
+ },
80
+ },
81
+ "repository": {
82
+ "node": {
83
+ "__typename": None,
84
+ "id": None,
85
+ "name": {
86
+ "value": None,
87
+ },
88
+ f"... on {InfrahubKind.REPOSITORY}": {
89
+ "commit": {
90
+ "value": None,
91
+ },
92
+ },
93
+ f"... on {InfrahubKind.READONLYREPOSITORY}": {
94
+ "commit": {
95
+ "value": None,
96
+ },
97
+ },
98
+ },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ )
105
+
15
106
 
16
107
  @flow(
17
108
  name="action-add-node-to-group",
@@ -65,12 +156,19 @@ async def run_generator(
65
156
  branch_name: str,
66
157
  node_ids: list[str],
67
158
  generator_definition_id: str,
68
- context: InfrahubContext, # noqa: ARG001
69
- service: InfrahubServices,
159
+ context: InfrahubContext,
160
+ service: InfrahubServices, # noqa: ARG001
70
161
  ) -> None:
71
162
  await add_tags(branches=[branch_name], nodes=node_ids + [generator_definition_id])
72
- await _run_generator(
73
- branch_name=branch_name, generator_definition_id=generator_definition_id, node_ids=node_ids, service=service
163
+
164
+ client = get_client()
165
+
166
+ await _run_generators(
167
+ branch_name=branch_name,
168
+ node_ids=node_ids,
169
+ generator_definition_id=generator_definition_id,
170
+ client=client,
171
+ context=context,
74
172
  )
75
173
 
76
174
 
@@ -82,13 +180,20 @@ async def run_generator_group_event(
82
180
  branch_name: str,
83
181
  members: list[EventGroupMember],
84
182
  generator_definition_id: str,
85
- context: InfrahubContext, # noqa: ARG001
86
- service: InfrahubServices,
183
+ context: InfrahubContext,
184
+ service: InfrahubServices, # noqa: ARG001
87
185
  ) -> None:
88
186
  node_ids = [node.id for node in members]
89
187
  await add_tags(branches=[branch_name], nodes=node_ids + [generator_definition_id])
90
- await _run_generator(
91
- branch_name=branch_name, generator_definition_id=generator_definition_id, node_ids=node_ids, service=service
188
+
189
+ client = get_client()
190
+
191
+ await _run_generators(
192
+ branch_name=branch_name,
193
+ node_ids=node_ids,
194
+ generator_definition_id=generator_definition_id,
195
+ client=client,
196
+ context=context,
92
197
  )
93
198
 
94
199
 
@@ -104,16 +209,95 @@ async def configure_action_rules(
104
209
  ) # type: ignore[misc]
105
210
 
106
211
 
107
- async def _run_generator(
212
+ async def _get_targets(
213
+ branch_name: str,
214
+ targets: list[dict[str, Any]],
215
+ client: InfrahubClient,
216
+ ) -> dict[str, dict[str, InfrahubNode]]:
217
+ """Get the targets per kind in order to extract the variables."""
218
+
219
+ targets_per_kind: dict[str, dict[str, InfrahubNode]] = defaultdict(dict)
220
+
221
+ for target in targets:
222
+ targets_per_kind[target["node"]["__typename"]][target["node"]["id"]] = None
223
+
224
+ for kind, values in targets_per_kind.items():
225
+ nodes = await client.filters(
226
+ kind=kind, branch=branch_name, ids=list(values.keys()), populate_store=False, order=Order(disable=True)
227
+ )
228
+ for node in nodes:
229
+ targets_per_kind[kind][node.id] = node
230
+
231
+ return targets_per_kind
232
+
233
+
234
+ async def _run_generators(
108
235
  branch_name: str,
109
236
  node_ids: list[str],
110
237
  generator_definition_id: str,
111
- service: InfrahubServices,
238
+ client: InfrahubClient,
239
+ context: InfrahubContext | None = None,
112
240
  ) -> None:
113
- mutation = Mutation(
114
- mutation="CoreGeneratorDefinitionRun",
115
- input_data={"data": {"id": generator_definition_id, "nodes": node_ids}},
116
- query={"ok": None},
241
+ """Fetch generator metadata and submit per-target runs.
242
+
243
+ Args:
244
+ branch_name: Branch on which to execute.
245
+ node_ids: Node IDs to run against (restricts selection if provided).
246
+ generator_definition_id: Generator definition to execute.
247
+ client: InfrahubClient to query additional data.
248
+ context: Execution context passed to downstream workflow submissions.
249
+
250
+ Returns:
251
+ None
252
+
253
+ Raises:
254
+ ValueError: If the generator definition is not found or none of the requested
255
+ targets are members of the target group.
256
+ """
257
+ response = await client.execute_graphql(
258
+ query=get_generator_run_query(definition_id=generator_definition_id, target_ids=node_ids).render(),
259
+ branch_name=branch_name,
117
260
  )
261
+ if not response[InfrahubKind.GENERATORDEFINITION]["edges"]:
262
+ raise ValueError(f"Generator definition {generator_definition_id} not found")
118
263
 
119
- await service.client.execute_graphql(query=mutation.render(), branch_name=branch_name)
264
+ data = response[InfrahubKind.GENERATORDEFINITION]["edges"][0]["node"]
265
+
266
+ if not data["targets"]["node"]["members"]["edges"]:
267
+ raise ValueError(f"Target {node_ids[0]} is not part of the group {data['targets']['node']['id']}")
268
+
269
+ targets = data["targets"]["node"]["members"]["edges"]
270
+
271
+ targets_per_kind = await _get_targets(branch_name=branch_name, targets=targets, client=client)
272
+
273
+ workflow = get_workflow()
274
+
275
+ for target in targets:
276
+ node: InfrahubNode | None = None
277
+ if data["parameters"]["value"]:
278
+ node = targets_per_kind[target["node"]["__typename"]][target["node"]["id"]]
279
+
280
+ request_generator_run_model = RequestGeneratorRun(
281
+ generator_definition=GeneratorDefinitionModel(
282
+ definition_id=generator_definition_id,
283
+ definition_name=data["name"]["value"],
284
+ class_name=data["class_name"]["value"],
285
+ file_path=data["file_path"]["value"],
286
+ query_name=data["query"]["node"]["name"]["value"],
287
+ convert_query_response=data["convert_query_response"]["value"],
288
+ group_id=data["targets"]["node"]["id"],
289
+ parameters=data["parameters"]["value"],
290
+ ),
291
+ commit=data["repository"]["node"]["commit"]["value"],
292
+ repository_id=data["repository"]["node"]["id"],
293
+ repository_name=data["repository"]["node"]["name"]["value"],
294
+ repository_kind=data["repository"]["node"]["__typename"],
295
+ branch_name=branch_name,
296
+ query=data["query"]["node"]["name"]["value"],
297
+ variables=await node.extract(params=data["parameters"]["value"]) if node else {},
298
+ target_id=target["node"]["id"],
299
+ target_name=target["node"]["display_label"],
300
+ )
301
+ await workflow.submit_workflow(
302
+ workflow=REQUEST_GENERATOR_RUN, context=context, parameters={"model": request_generator_run_model}
303
+ )
infrahub/api/artifact.py CHANGED
@@ -15,6 +15,7 @@ from infrahub.api.dependencies import (
15
15
  )
16
16
  from infrahub.core import registry
17
17
  from infrahub.core.account import ObjectPermission
18
+ from infrahub.core.branch.needs_rebase_status import check_need_rebase_status
18
19
  from infrahub.core.constants import GLOBAL_BRANCH_NAME, InfrahubKind, PermissionAction
19
20
  from infrahub.core.protocols import CoreArtifactDefinition
20
21
  from infrahub.database import InfrahubDatabase # noqa: TC001
@@ -74,6 +75,8 @@ async def generate_artifact(
74
75
  permission_manager: PermissionManager = Depends(get_permission_manager),
75
76
  context: InfrahubContext = Depends(get_context),
76
77
  ) -> None:
78
+ check_need_rebase_status(branch_params.branch)
79
+
77
80
  permission_decision = (
78
81
  PermissionDecisionFlag.ALLOW_DEFAULT
79
82
  if branch_params.branch.name in (GLOBAL_BRANCH_NAME, registry.default_branch)
infrahub/api/query.py CHANGED
@@ -24,6 +24,7 @@ from infrahub.graphql.metrics import (
24
24
  GRAPHQL_RESPONSE_SIZE_METRICS,
25
25
  GRAPHQL_TOP_LEVEL_QUERIES_METRICS,
26
26
  )
27
+ from infrahub.graphql.middleware import raise_on_mutation_on_branch_needing_rebase
27
28
  from infrahub.graphql.utils import extract_data
28
29
  from infrahub.groups.models import RequestGraphQLQueryGroupUpdate
29
30
  from infrahub.log import get_logger
@@ -98,6 +99,7 @@ async def execute_query(
98
99
  context_value=gql_params.context,
99
100
  root_value=None,
100
101
  variable_values=params,
102
+ middleware=[raise_on_mutation_on_branch_needing_rebase],
101
103
  )
102
104
 
103
105
  data = extract_data(query_name=gql_query.name.value, result=result)
infrahub/api/schema.py CHANGED
@@ -18,6 +18,7 @@ from infrahub.api.exceptions import SchemaNotValidError
18
18
  from infrahub.core import registry
19
19
  from infrahub.core.account import GlobalPermission
20
20
  from infrahub.core.branch import Branch # noqa: TC001
21
+ from infrahub.core.branch.needs_rebase_status import check_need_rebase_status
21
22
  from infrahub.core.constants import GLOBAL_BRANCH_NAME, GlobalPermissions, PermissionDecision
22
23
  from infrahub.core.migrations.schema.models import SchemaApplyMigrationData
23
24
  from infrahub.core.models import ( # noqa: TC001
@@ -287,6 +288,8 @@ async def load_schema(
287
288
  permission_manager: PermissionManager = Depends(get_permission_manager),
288
289
  context: InfrahubContext = Depends(get_context),
289
290
  ) -> SchemaUpdate:
291
+ check_need_rebase_status(branch)
292
+
290
293
  permission_manager.raise_for_permission(
291
294
  permission=define_global_permission_from_branch(
292
295
  permission=GlobalPermissions.MANAGE_SCHEMA, branch_name=branch.name
infrahub/auth.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import uuid
4
- from datetime import datetime, timedelta, timezone
4
+ from datetime import UTC, datetime, timedelta
5
5
  from enum import Enum
6
6
  from typing import TYPE_CHECKING
7
7
 
@@ -78,7 +78,7 @@ async def authenticate_with_password(
78
78
  if not valid_credentials:
79
79
  raise AuthorizationError("Incorrect password")
80
80
 
81
- now = datetime.now(tz=timezone.utc)
81
+ now = datetime.now(tz=UTC)
82
82
  refresh_expires = now + timedelta(seconds=config.SETTINGS.security.refresh_token_lifetime)
83
83
 
84
84
  session_id = await create_db_refresh_token(db=db, account_id=account.id, expiration=refresh_expires)
@@ -139,7 +139,7 @@ async def signin_sso_account(db: InfrahubDatabase, account_name: str, sso_groups
139
139
  await group.members.add(db=db, data=account)
140
140
  await group.members.save(db=db)
141
141
 
142
- now = datetime.now(tz=timezone.utc)
142
+ now = datetime.now(tz=UTC)
143
143
  refresh_expires = now + timedelta(seconds=config.SETTINGS.security.refresh_token_lifetime)
144
144
  session_id = await create_db_refresh_token(db=db, account_id=account.id, expiration=refresh_expires)
145
145
  access_token = generate_access_token(account_id=account.id, session_id=session_id)
@@ -148,7 +148,7 @@ async def signin_sso_account(db: InfrahubDatabase, account_name: str, sso_groups
148
148
 
149
149
 
150
150
  def generate_access_token(account_id: str, session_id: uuid.UUID) -> str:
151
- now = datetime.now(tz=timezone.utc)
151
+ now = datetime.now(tz=UTC)
152
152
 
153
153
  access_expires = now + timedelta(seconds=config.SETTINGS.security.access_token_lifetime)
154
154
  access_data = {
@@ -165,7 +165,7 @@ def generate_access_token(account_id: str, session_id: uuid.UUID) -> str:
165
165
 
166
166
 
167
167
  def generate_refresh_token(account_id: str, session_id: uuid.UUID, expiration: datetime) -> str:
168
- now = datetime.now(tz=timezone.utc)
168
+ now = datetime.now(tz=UTC)
169
169
 
170
170
  refresh_data = {
171
171
  "sub": account_id,
infrahub/cli/db.py CHANGED
@@ -5,7 +5,7 @@ import logging
5
5
  import os
6
6
  from collections import defaultdict
7
7
  from csv import DictReader, DictWriter
8
- from datetime import datetime, timezone
8
+ from datetime import UTC, datetime
9
9
  from enum import Enum
10
10
  from pathlib import Path
11
11
  from typing import TYPE_CHECKING, Any, Sequence
@@ -59,7 +59,7 @@ from .patch import patch_app
59
59
 
60
60
  def get_timestamp_string() -> str:
61
61
  """Generate a timestamp string in the format YYYYMMDD-HHMMSS."""
62
- return datetime.now(tz=timezone.utc).strftime("%Y%m%d-%H%M%S")
62
+ return datetime.now(tz=UTC).strftime("%Y%m%d-%H%M%S")
63
63
 
64
64
 
65
65
  if TYPE_CHECKING:
infrahub/config.py CHANGED
@@ -8,7 +8,7 @@ from enum import Enum
8
8
  from pathlib import Path
9
9
  from typing import TYPE_CHECKING, Any
10
10
 
11
- import toml
11
+ import tomllib
12
12
  from infrahub_sdk.utils import generate_uuid
13
13
  from pydantic import (
14
14
  AliasChoices,
@@ -371,6 +371,11 @@ class CacheSettings(BaseSettings):
371
371
  tls_enabled: bool = Field(default=False, description="Indicates if TLS is enabled for the connection")
372
372
  tls_insecure: bool = Field(default=False, description="Indicates if TLS certificates are verified")
373
373
  tls_ca_file: str | None = Field(default=None, description="File path to CA cert or bundle in PEM format")
374
+ clean_up_deadlocks_interval_mins: int = Field(
375
+ default=15,
376
+ ge=1,
377
+ description="Age threshold in minutes: locks older than this and owned by inactive workers are deleted by the cleanup task.",
378
+ )
374
379
 
375
380
  @property
376
381
  def service_port(self) -> int:
@@ -975,7 +980,7 @@ def load(config_file_name: Path | str = "infrahub.toml", config_data: dict[str,
975
980
 
976
981
  if config_file.exists():
977
982
  config_string = config_file.read_text(encoding="utf-8")
978
- config_tmp = toml.loads(config_string)
983
+ config_tmp = tomllib.loads(config_string)
979
984
 
980
985
  return Settings(**config_tmp)
981
986
 
@@ -324,7 +324,7 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
324
324
 
325
325
  save_at = Timestamp(at)
326
326
 
327
- if not self.id or self.is_from_profile:
327
+ if not self.id:
328
328
  return None
329
329
 
330
330
  return await self._update(at=save_at, db=db)
@@ -395,7 +395,6 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
395
395
 
396
396
  Get the current value
397
397
  - If the value is the same, do nothing
398
- - If the value is inherited and is different, raise error (for now just ignore)
399
398
  - If the value is different, create new node and update relationship
400
399
 
401
400
  """
@@ -470,28 +469,32 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
470
469
 
471
470
  # ---------- Update the Node Properties ----------
472
471
  for prop_name in self._node_properties:
473
- if getattr(self, f"{prop_name}_id") and not (
474
- prop_name in current_attr_data.node_properties
475
- and current_attr_data.node_properties[prop_name].uuid == getattr(self, f"{prop_name}_id")
476
- ):
477
- previous_attribute_node_property = current_attr_data.node_properties.get(prop_name)
478
- previous_value = None
479
- if previous_attribute_node_property:
480
- previous_value = previous_attribute_node_property.uuid
472
+ current_prop_id = getattr(self, f"{prop_name}_id")
473
+ database_prop_id: str | None = None
474
+ if prop_name in current_attr_data.node_properties:
475
+ database_prop_id = current_attr_data.node_properties[prop_name].uuid
476
+ needs_update = current_prop_id is not None and current_prop_id != database_prop_id
477
+ needs_clear = self.is_clear(prop_name) and database_prop_id
478
+
479
+ if not needs_update and not needs_clear:
480
+ continue
481
481
 
482
- changelog.add_property(
483
- name=prop_name,
484
- value_current=getattr(self, f"{prop_name}_id"),
485
- value_previous=previous_value,
486
- )
482
+ changelog.add_property(
483
+ name=prop_name,
484
+ value_current=current_prop_id,
485
+ value_previous=database_prop_id,
486
+ )
487
+
488
+ if needs_update:
487
489
  query = await AttributeUpdateNodePropertyQuery.init(
488
- db=db, attr=self, at=update_at, prop_name=prop_name, prop_id=getattr(self, f"{prop_name}_id")
490
+ db=db, attr=self, at=update_at, prop_name=prop_name, prop_id=current_prop_id
489
491
  )
490
492
  await query.execute(db=db)
491
493
 
492
- rel = current_attr_result.get(f"rel_{prop_name}")
493
- if rel and rel.get("branch") == branch.name:
494
- await update_relationships_to([rel.element_id], to=update_at, db=db)
494
+ # set the to time on the previously active edge
495
+ rel = current_attr_result.get(f"rel_{prop_name}")
496
+ if rel and rel.get("branch") == branch.name:
497
+ await update_relationships_to([rel.element_id], to=update_at, db=db)
495
498
 
496
499
  if changelog.has_updates:
497
500
  return changelog
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Optional, Self, Union
5
5
 
6
6
  from pydantic import Field, field_validator
7
7
 
8
+ from infrahub.core.branch.enums import BranchStatus
8
9
  from infrahub.core.constants import (
9
10
  GLOBAL_BRANCH_NAME,
10
11
  )
@@ -21,8 +22,6 @@ from infrahub.core.registry import registry
21
22
  from infrahub.core.timestamp import Timestamp
22
23
  from infrahub.exceptions import BranchNotFoundError, InitializationError, ValidationError
23
24
 
24
- from .enums import BranchStatus
25
-
26
25
  if TYPE_CHECKING:
27
26
  from infrahub.database import InfrahubDatabase
28
27
 
@@ -485,6 +484,7 @@ class Branch(StandardNode):
485
484
  # FIXME, we must ensure that there is no conflict before rebasing a branch
486
485
  # Otherwise we could endup with a complicated situation
487
486
  self.branched_from = at.to_string()
487
+ self.status = BranchStatus.OPEN
488
488
  await self.save(db=db)
489
489
 
490
490
  # Update the branch in the registry after the rebase
@@ -0,0 +1,11 @@
1
+ from infrahub.core.branch import Branch
2
+ from infrahub.core.branch.enums import BranchStatus
3
+
4
+
5
+ def raise_needs_rebase_error(branch_name: str) -> None:
6
+ raise ValueError(f"Branch {branch_name} must be rebased before any updates can be made")
7
+
8
+
9
+ def check_need_rebase_status(branch: Branch) -> None:
10
+ if branch.status == BranchStatus.NEED_REBASE:
11
+ raise_needs_rebase_error(branch_name=branch.name)
@@ -345,7 +345,7 @@ async def create_branch(model: BranchCreateModel, context: InfrahubContext) -> N
345
345
  async with database.start_session() as db:
346
346
  try:
347
347
  await Branch.get_by_name(db=db, name=model.name)
348
- raise ValueError(f"The branch {model.name}, already exist")
348
+ raise ValidationError(f"The branch {model.name} already exists")
349
349
  except BranchNotFoundError:
350
350
  pass
351
351
 
@@ -356,7 +356,7 @@ async def create_branch(model: BranchCreateModel, context: InfrahubContext) -> N
356
356
  obj = Branch(**data_dict)
357
357
  except pydantic.ValidationError as exc:
358
358
  error_msgs = [f"invalid field {error['loc'][0]}: {error['msg']}" for error in exc.errors()]
359
- raise ValueError("\n".join(error_msgs)) from exc
359
+ raise ValidationError("\n".join(error_msgs)) from exc
360
360
 
361
361
  async with lock.registry.local_schema_lock():
362
362
  # Copy the schema from the origin branch and set the hash and the schema_changed_at value
@@ -387,3 +387,4 @@ DEFAULT_REL_IDENTIFIER_LENGTH = 128
387
387
 
388
388
  OBJECT_TEMPLATE_RELATIONSHIP_NAME = "object_template"
389
389
  OBJECT_TEMPLATE_NAME_ATTR = "template_name"
390
+ PROFILE_NODE_RELATIONSHIP_IDENTIFIER = "node__profile"