infrahub-server 1.4.10__py3-none-any.whl → 1.5.0b1__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 (178) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/query.py +2 -0
  5. infrahub/api/schema.py +3 -0
  6. infrahub/auth.py +5 -5
  7. infrahub/cli/db.py +26 -2
  8. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  9. infrahub/config.py +7 -2
  10. infrahub/core/attribute.py +25 -22
  11. infrahub/core/branch/models.py +2 -2
  12. infrahub/core/branch/needs_rebase_status.py +11 -0
  13. infrahub/core/branch/tasks.py +4 -3
  14. infrahub/core/changelog/models.py +4 -12
  15. infrahub/core/constants/__init__.py +1 -0
  16. infrahub/core/constants/infrahubkind.py +1 -0
  17. infrahub/core/convert_object_type/object_conversion.py +201 -0
  18. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  19. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  20. infrahub/core/diff/model/path.py +4 -0
  21. infrahub/core/diff/payload_builder.py +1 -1
  22. infrahub/core/diff/query/artifact.py +1 -1
  23. infrahub/core/graph/__init__.py +1 -1
  24. infrahub/core/initialization.py +2 -2
  25. infrahub/core/ipam/utilization.py +1 -1
  26. infrahub/core/manager.py +9 -84
  27. infrahub/core/migrations/graph/__init__.py +6 -0
  28. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  29. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
  30. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
  31. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  32. infrahub/core/migrations/shared.py +5 -6
  33. infrahub/core/node/__init__.py +165 -42
  34. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  35. infrahub/core/node/create.py +67 -35
  36. infrahub/core/node/lock_utils.py +98 -0
  37. infrahub/core/node/node_property_attribute.py +230 -0
  38. infrahub/core/node/standard.py +1 -1
  39. infrahub/core/property.py +11 -0
  40. infrahub/core/protocols.py +8 -1
  41. infrahub/core/query/attribute.py +27 -15
  42. infrahub/core/query/node.py +61 -185
  43. infrahub/core/query/relationship.py +43 -26
  44. infrahub/core/query/subquery.py +0 -8
  45. infrahub/core/registry.py +2 -2
  46. infrahub/core/relationship/constraints/count.py +1 -1
  47. infrahub/core/relationship/model.py +60 -20
  48. infrahub/core/schema/attribute_schema.py +0 -2
  49. infrahub/core/schema/basenode_schema.py +42 -2
  50. infrahub/core/schema/definitions/core/__init__.py +2 -0
  51. infrahub/core/schema/definitions/core/generator.py +2 -0
  52. infrahub/core/schema/definitions/core/group.py +16 -2
  53. infrahub/core/schema/definitions/core/repository.py +7 -0
  54. infrahub/core/schema/definitions/internal.py +14 -1
  55. infrahub/core/schema/generated/base_node_schema.py +6 -1
  56. infrahub/core/schema/node_schema.py +5 -2
  57. infrahub/core/schema/relationship_schema.py +0 -1
  58. infrahub/core/schema/schema_branch.py +137 -2
  59. infrahub/core/schema/schema_branch_display.py +123 -0
  60. infrahub/core/schema/schema_branch_hfid.py +114 -0
  61. infrahub/core/validators/aggregated_checker.py +1 -1
  62. infrahub/core/validators/determiner.py +12 -1
  63. infrahub/core/validators/relationship/peer.py +1 -1
  64. infrahub/core/validators/tasks.py +1 -1
  65. infrahub/display_labels/__init__.py +0 -0
  66. infrahub/display_labels/gather.py +48 -0
  67. infrahub/display_labels/models.py +240 -0
  68. infrahub/display_labels/tasks.py +186 -0
  69. infrahub/display_labels/triggers.py +22 -0
  70. infrahub/events/group_action.py +1 -1
  71. infrahub/events/node_action.py +1 -1
  72. infrahub/generators/constants.py +7 -0
  73. infrahub/generators/models.py +38 -12
  74. infrahub/generators/tasks.py +34 -16
  75. infrahub/git/base.py +38 -1
  76. infrahub/git/integrator.py +22 -14
  77. infrahub/graphql/analyzer.py +1 -1
  78. infrahub/graphql/api/dependencies.py +2 -4
  79. infrahub/graphql/api/endpoints.py +2 -2
  80. infrahub/graphql/app.py +2 -4
  81. infrahub/graphql/initialization.py +2 -3
  82. infrahub/graphql/manager.py +212 -137
  83. infrahub/graphql/middleware.py +12 -0
  84. infrahub/graphql/mutations/branch.py +11 -0
  85. infrahub/graphql/mutations/computed_attribute.py +110 -3
  86. infrahub/graphql/mutations/convert_object_type.py +34 -13
  87. infrahub/graphql/mutations/display_label.py +111 -0
  88. infrahub/graphql/mutations/generator.py +25 -7
  89. infrahub/graphql/mutations/hfid.py +118 -0
  90. infrahub/graphql/mutations/ipam.py +21 -8
  91. infrahub/graphql/mutations/main.py +37 -153
  92. infrahub/graphql/mutations/profile.py +195 -0
  93. infrahub/graphql/mutations/proposed_change.py +2 -1
  94. infrahub/graphql/mutations/relationship.py +2 -2
  95. infrahub/graphql/mutations/repository.py +22 -83
  96. infrahub/graphql/mutations/resource_manager.py +2 -2
  97. infrahub/graphql/mutations/schema.py +5 -5
  98. infrahub/graphql/mutations/webhook.py +1 -1
  99. infrahub/graphql/queries/resource_manager.py +1 -1
  100. infrahub/graphql/registry.py +173 -0
  101. infrahub/graphql/resolvers/resolver.py +2 -0
  102. infrahub/graphql/schema.py +8 -1
  103. infrahub/groups/tasks.py +1 -1
  104. infrahub/hfid/__init__.py +0 -0
  105. infrahub/hfid/gather.py +48 -0
  106. infrahub/hfid/models.py +240 -0
  107. infrahub/hfid/tasks.py +185 -0
  108. infrahub/hfid/triggers.py +22 -0
  109. infrahub/lock.py +67 -30
  110. infrahub/locks/__init__.py +0 -0
  111. infrahub/locks/tasks.py +37 -0
  112. infrahub/middleware.py +26 -1
  113. infrahub/patch/plan_writer.py +2 -2
  114. infrahub/profiles/__init__.py +0 -0
  115. infrahub/profiles/node_applier.py +101 -0
  116. infrahub/profiles/queries/__init__.py +0 -0
  117. infrahub/profiles/queries/get_profile_data.py +99 -0
  118. infrahub/profiles/tasks.py +63 -0
  119. infrahub/proposed_change/tasks.py +10 -1
  120. infrahub/repositories/__init__.py +0 -0
  121. infrahub/repositories/create_repository.py +113 -0
  122. infrahub/server.py +16 -3
  123. infrahub/services/__init__.py +8 -5
  124. infrahub/tasks/registry.py +6 -4
  125. infrahub/trigger/catalogue.py +4 -0
  126. infrahub/trigger/models.py +2 -0
  127. infrahub/trigger/tasks.py +3 -0
  128. infrahub/webhook/models.py +1 -1
  129. infrahub/workflows/catalogue.py +110 -3
  130. infrahub/workflows/initialization.py +16 -0
  131. infrahub/workflows/models.py +17 -2
  132. infrahub_sdk/branch.py +5 -8
  133. infrahub_sdk/checks.py +1 -1
  134. infrahub_sdk/client.py +364 -84
  135. infrahub_sdk/convert_object_type.py +61 -0
  136. infrahub_sdk/ctl/check.py +2 -3
  137. infrahub_sdk/ctl/cli_commands.py +18 -12
  138. infrahub_sdk/ctl/config.py +8 -2
  139. infrahub_sdk/ctl/generator.py +6 -3
  140. infrahub_sdk/ctl/graphql.py +184 -0
  141. infrahub_sdk/ctl/repository.py +39 -1
  142. infrahub_sdk/ctl/schema.py +18 -3
  143. infrahub_sdk/ctl/utils.py +4 -0
  144. infrahub_sdk/ctl/validate.py +5 -3
  145. infrahub_sdk/diff.py +4 -5
  146. infrahub_sdk/exceptions.py +2 -0
  147. infrahub_sdk/generator.py +7 -1
  148. infrahub_sdk/graphql/__init__.py +12 -0
  149. infrahub_sdk/graphql/constants.py +1 -0
  150. infrahub_sdk/graphql/plugin.py +85 -0
  151. infrahub_sdk/graphql/query.py +77 -0
  152. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  153. infrahub_sdk/graphql/utils.py +40 -0
  154. infrahub_sdk/node/attribute.py +2 -0
  155. infrahub_sdk/node/node.py +28 -20
  156. infrahub_sdk/playback.py +1 -2
  157. infrahub_sdk/protocols.py +54 -6
  158. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  159. infrahub_sdk/pytest_plugin/utils.py +40 -0
  160. infrahub_sdk/repository.py +1 -2
  161. infrahub_sdk/schema/__init__.py +38 -0
  162. infrahub_sdk/schema/main.py +1 -0
  163. infrahub_sdk/schema/repository.py +8 -0
  164. infrahub_sdk/spec/object.py +120 -7
  165. infrahub_sdk/spec/range_expansion.py +118 -0
  166. infrahub_sdk/timestamp.py +18 -6
  167. infrahub_sdk/transforms.py +1 -1
  168. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +9 -11
  169. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +177 -134
  170. infrahub_testcontainers/container.py +1 -1
  171. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  172. infrahub_testcontainers/docker-compose.test.yml +1 -1
  173. infrahub_testcontainers/models.py +2 -2
  174. infrahub_testcontainers/performance_test.py +4 -4
  175. infrahub/core/convert_object_type/conversion.py +0 -134
  176. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
  177. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
  178. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
infrahub/core/manager.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from copy import copy
4
- from functools import reduce
5
4
  from typing import TYPE_CHECKING, Any, Iterable, Literal, TypeVar, overload
6
5
 
7
6
  from infrahub_sdk.utils import deep_merge_dict, is_valid_uuid
@@ -11,9 +10,7 @@ from infrahub.core.node import Node
11
10
  from infrahub.core.node.delete_validator import NodeDeleteValidator
12
11
  from infrahub.core.query.node import (
13
12
  AttributeFromDB,
14
- AttributeNodePropertyFromDB,
15
13
  GroupedPeerNodes,
16
- NodeAttributesFromDB,
17
14
  NodeGetHierarchyQuery,
18
15
  NodeGetListQuery,
19
16
  NodeListGetAttributeQuery,
@@ -63,72 +60,21 @@ def identify_node_class(node: NodeToProcess) -> type[Node]:
63
60
 
64
61
 
65
62
  def get_schema(
66
- db: InfrahubDatabase, branch: Branch, node_schema: type[SchemaProtocol] | MainSchemaTypes | str
63
+ db: InfrahubDatabase,
64
+ branch: Branch,
65
+ node_schema: type[SchemaProtocol] | MainSchemaTypes | str,
66
+ duplicate: bool = False,
67
67
  ) -> MainSchemaTypes:
68
68
  if isinstance(node_schema, str):
69
- return db.schema.get(name=node_schema, branch=branch.name)
69
+ return db.schema.get(name=node_schema, branch=branch.name, duplicate=duplicate)
70
70
  if hasattr(node_schema, "_is_runtime_protocol") and node_schema._is_runtime_protocol:
71
- return db.schema.get(name=node_schema.__name__, branch=branch.name)
71
+ return db.schema.get(name=node_schema.__name__, branch=branch.name, duplicate=duplicate)
72
72
  if not isinstance(node_schema, (MainSchemaTypes)):
73
73
  raise ValueError(f"Invalid schema provided {node_schema}")
74
74
 
75
75
  return node_schema
76
76
 
77
77
 
78
- class ProfileAttributeIndex:
79
- def __init__(
80
- self,
81
- profile_attributes_id_map: dict[str, NodeAttributesFromDB],
82
- profile_ids_by_node_id: dict[str, list[str]],
83
- ) -> None:
84
- self._profile_attributes_id_map = profile_attributes_id_map
85
- self._profile_ids_by_node_id = profile_ids_by_node_id
86
-
87
- def apply_profiles(self, node_data_dict: dict[str, Any]) -> dict[str, Any]:
88
- updated_data: dict[str, Any] = {**node_data_dict}
89
- node_id = node_data_dict.get("id")
90
- profile_ids = self._profile_ids_by_node_id.get(node_id, [])
91
- if not profile_ids:
92
- return updated_data
93
- profiles = [
94
- self._profile_attributes_id_map[p_id] for p_id in profile_ids if p_id in self._profile_attributes_id_map
95
- ]
96
-
97
- def get_profile_priority(nafd: NodeAttributesFromDB) -> tuple[int | float, str]:
98
- try:
99
- return (int(nafd.attrs.get("profile_priority").value), nafd.node.get("uuid"))
100
- except (TypeError, AttributeError):
101
- return (float("inf"), "")
102
-
103
- profiles.sort(key=get_profile_priority)
104
-
105
- for attr_name, attr_data in updated_data.items():
106
- if not isinstance(attr_data, AttributeFromDB):
107
- continue
108
- if not attr_data.is_default:
109
- continue
110
- profile_value, profile_uuid = None, None
111
- index = 0
112
-
113
- while profile_value is None and index <= (len(profiles) - 1):
114
- try:
115
- profile_value = profiles[index].attrs[attr_name].value
116
- if profile_value != "NULL":
117
- profile_uuid = profiles[index].node["uuid"]
118
- break
119
- profile_value = None
120
- except (IndexError, KeyError, AttributeError):
121
- ...
122
- index += 1
123
-
124
- if profile_value is not None:
125
- attr_data.value = profile_value
126
- attr_data.is_from_profile = True
127
- attr_data.is_default = False
128
- attr_data.node_properties["source"] = AttributeNodePropertyFromDB(uuid=profile_uuid, labels=[])
129
- return updated_data
130
-
131
-
132
78
  class NodeManager:
133
79
  @overload
134
80
  @classmethod
@@ -1129,21 +1075,11 @@ class NodeManager:
1129
1075
  )
1130
1076
  await query.execute(db=db)
1131
1077
  nodes_info_by_id: dict[str, NodeToProcess] = {node.node_uuid: node async for node in query.get_nodes(db=db)}
1132
- profile_ids_by_node_id = query.get_profile_ids_by_node_id()
1133
- all_profile_ids = reduce(
1134
- lambda all_ids, these_ids: all_ids | set(these_ids), profile_ids_by_node_id.values(), set()
1135
- )
1136
-
1137
- if fields and all_profile_ids:
1138
- if "profile_priority" not in fields:
1139
- fields["profile_priority"] = {}
1140
- if "value" not in fields["profile_priority"]:
1141
- fields["profile_priority"]["value"] = None
1142
1078
 
1143
1079
  # Query list of all Attributes
1144
1080
  query = await NodeListGetAttributeQuery.init(
1145
1081
  db=db,
1146
- ids=list(nodes_info_by_id.keys()) + list(all_profile_ids),
1082
+ ids=list(nodes_info_by_id.keys()),
1147
1083
  fields=fields,
1148
1084
  branch=branch,
1149
1085
  include_source=include_source,
@@ -1153,17 +1089,7 @@ class NodeManager:
1153
1089
  branch_agnostic=branch_agnostic,
1154
1090
  )
1155
1091
  await query.execute(db=db)
1156
- all_node_attributes = query.get_attributes_group_by_node()
1157
- profile_attributes: dict[str, dict[str, AttributeFromDB]] = {}
1158
- node_attributes: dict[str, dict[str, AttributeFromDB]] = {}
1159
- for node_id, attribute_dict in all_node_attributes.items():
1160
- if node_id in all_profile_ids:
1161
- profile_attributes[node_id] = attribute_dict
1162
- else:
1163
- node_attributes[node_id] = attribute_dict
1164
- profile_index = ProfileAttributeIndex(
1165
- profile_attributes_id_map=profile_attributes, profile_ids_by_node_id=profile_ids_by_node_id
1166
- )
1092
+ node_attributes = query.get_attributes_group_by_node()
1167
1093
 
1168
1094
  nodes: dict[str, Node] = {}
1169
1095
 
@@ -1192,11 +1118,10 @@ class NodeManager:
1192
1118
  for attr_name, attr in node_attributes[node_id].attrs.items():
1193
1119
  new_node_data[attr_name] = attr
1194
1120
 
1195
- new_node_data_with_profile_overrides = profile_index.apply_profiles(new_node_data)
1196
1121
  node_class = identify_node_class(node=node)
1197
1122
  node_branch = await registry.get_branch(db=db, branch=node.branch)
1198
1123
  item = await node_class.init(schema=node.schema, branch=node_branch, at=at, db=db)
1199
- await item.load(**new_node_data_with_profile_overrides, db=db)
1124
+ await item.load(**new_node_data, db=db)
1200
1125
 
1201
1126
  nodes[node_id] = item
1202
1127
 
@@ -41,6 +41,9 @@ from .m036_drop_attr_value_index import Migration036
41
41
  from .m037_index_attr_vals import Migration037
42
42
  from .m038_redo_0000_prefix_fix import Migration038
43
43
  from .m039_ipam_reconcile import Migration039
44
+ from .m040_profile_attrs_in_db import Migration040
45
+ from .m041_create_hfid_display_label_in_db import Migration041
46
+ from .m042_backfill_hfid_display_label_in_db import Migration042
44
47
 
45
48
  if TYPE_CHECKING:
46
49
  from infrahub.core.root import Root
@@ -87,6 +90,9 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
87
90
  Migration037,
88
91
  Migration038,
89
92
  Migration039,
93
+ Migration040,
94
+ Migration041,
95
+ Migration042,
90
96
  ]
91
97
 
92
98
 
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from rich.console import Console
7
+ from rich.progress import Progress
8
+
9
+ from infrahub.core.branch.models import Branch
10
+ from infrahub.core.initialization import initialization
11
+ from infrahub.core.manager import NodeManager
12
+ from infrahub.core.migrations.shared import MigrationResult
13
+ from infrahub.core.query import Query, QueryType
14
+ from infrahub.core.timestamp import Timestamp
15
+ from infrahub.lock import initialize_lock
16
+ from infrahub.log import get_logger
17
+ from infrahub.profiles.node_applier import NodeProfilesApplier
18
+
19
+ from ..shared import ArbitraryMigration
20
+
21
+ if TYPE_CHECKING:
22
+ from infrahub.core.node import Node
23
+ from infrahub.database import InfrahubDatabase
24
+
25
+ log = get_logger()
26
+
27
+
28
+ class GetProfilesByBranchQuery(Query):
29
+ """
30
+ Get CoreProfile UUIDs by which branches they have attribute updates on
31
+ """
32
+
33
+ name = "get_profiles_by_branch"
34
+ type = QueryType.READ
35
+ insert_return = False
36
+
37
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
38
+ query = """
39
+ MATCH (profile:CoreProfile)-[:HAS_ATTRIBUTE]->(attr:Attribute)-[e:HAS_VALUE]->(:AttributeValue)
40
+ WITH DISTINCT profile.uuid AS profile_uuid, e.branch AS branch
41
+ RETURN profile_uuid, collect(branch) AS branches
42
+ """
43
+ self.add_to_query(query)
44
+ self.return_labels = ["profile_uuid", "branches"]
45
+
46
+ def get_profile_ids_by_branch(self) -> dict[str, set[str]]:
47
+ """Get dictionary of branch names to set of updated profile UUIDs"""
48
+ profiles_by_branch = defaultdict(set)
49
+ for result in self.get_results():
50
+ profile_uuid = result.get_as_type("profile_uuid", str)
51
+ branches = result.get_as_type("branches", list[str])
52
+ for branch in branches:
53
+ profiles_by_branch[branch].add(profile_uuid)
54
+ return profiles_by_branch
55
+
56
+
57
+ class GetNodesWithProfileUpdatesByBranchQuery(Query):
58
+ """
59
+ Get Node UUIDs by which branches they have updated profiles on
60
+ """
61
+
62
+ name = "get_nodes_with_profile_updates_by_branch"
63
+ type = QueryType.READ
64
+ insert_return = False
65
+
66
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
67
+ query = """
68
+ MATCH (node:Node)-[e1:IS_RELATED]->(:Relationship {name: "node__profile"})
69
+ WHERE NOT node:CoreProfile
70
+ WITH DISTINCT node.uuid AS node_uuid, e1.branch AS branch
71
+ RETURN node_uuid, collect(branch) AS branches
72
+ """
73
+ self.add_to_query(query)
74
+ self.return_labels = ["node_uuid", "branches"]
75
+
76
+ def get_node_ids_by_branch(self) -> dict[str, set[str]]:
77
+ """Get dictionary of branch names to set of updated node UUIDs"""
78
+ nodes_by_branch = defaultdict(set)
79
+ for result in self.get_results():
80
+ node_uuid = result.get_as_type("node_uuid", str)
81
+ branches = result.get_as_type("branches", list[str])
82
+ for branch in branches:
83
+ nodes_by_branch[branch].add(node_uuid)
84
+ return nodes_by_branch
85
+
86
+
87
+ class Migration040(ArbitraryMigration):
88
+ """
89
+ Save profile attribute values on each node using the profile in the database
90
+ For any profile that has updates on a given branch (including default branch)
91
+ - run NodeProfilesApplier.apply_profiles on each node related to the profile on that branch
92
+ For any node that has an updated relationship to a profile on a given branch
93
+ - run NodeProfilesApplier.apply_profiles on the node on that branch
94
+ """
95
+
96
+ name: str = "040_profile_attrs_in_db"
97
+ minimum_version: int = 39
98
+
99
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
100
+ super().__init__(*args, **kwargs)
101
+ self._appliers_by_branch: dict[str, NodeProfilesApplier] = {}
102
+
103
+ async def _get_profile_applier(self, db: InfrahubDatabase, branch_name: str) -> NodeProfilesApplier:
104
+ if branch_name not in self._appliers_by_branch:
105
+ branch = await Branch.get_by_name(db=db, name=branch_name)
106
+ self._appliers_by_branch[branch_name] = NodeProfilesApplier(db=db, branch=branch)
107
+ return self._appliers_by_branch[branch_name]
108
+
109
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
110
+ return MigrationResult()
111
+
112
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
113
+ console = Console()
114
+ result = MigrationResult()
115
+ # load schemas from database into registry
116
+ initialize_lock()
117
+ await initialization(db=db)
118
+
119
+ console.print("Gathering profiles for each branch...", end="")
120
+ get_profiles_by_branch_query = await GetProfilesByBranchQuery.init(db=db)
121
+ await get_profiles_by_branch_query.execute(db=db)
122
+ profiles_ids_by_branch = get_profiles_by_branch_query.get_profile_ids_by_branch()
123
+
124
+ profiles_by_branch: dict[str, list[Node]] = {}
125
+ for branch_name, profile_ids in profiles_ids_by_branch.items():
126
+ profiles_map = await NodeManager.get_many(db=db, branch=branch_name, ids=list(profile_ids))
127
+ profiles_by_branch[branch_name] = list(profiles_map.values())
128
+ console.print("done")
129
+
130
+ node_ids_to_update_by_branch: dict[str, set[str]] = defaultdict(set)
131
+ total_size = sum(len(profiles) for profiles in profiles_by_branch.values())
132
+ with Progress() as progress:
133
+ gather_nodes_task = progress.add_task(
134
+ "Gathering affected objects for each profile on each branch...", total=total_size
135
+ )
136
+
137
+ for branch_name, profiles in profiles_by_branch.items():
138
+ for profile in profiles:
139
+ node_relationship_manager = profile.get_relationship("related_nodes")
140
+ node_peers = await node_relationship_manager.get_db_peers(db=db)
141
+ node_ids_to_update_by_branch[branch_name].update({str(peer.peer_id) for peer in node_peers})
142
+ progress.update(gather_nodes_task, advance=1)
143
+
144
+ console.print("Identifying nodes with profile updates by branch...", end="")
145
+ get_nodes_with_profile_updates_by_branch_query = await GetNodesWithProfileUpdatesByBranchQuery.init(db=db)
146
+ await get_nodes_with_profile_updates_by_branch_query.execute(db=db)
147
+ nodes_ids_by_branch = get_nodes_with_profile_updates_by_branch_query.get_node_ids_by_branch()
148
+ for branch_name, node_ids in nodes_ids_by_branch.items():
149
+ node_ids_to_update_by_branch[branch_name].update(node_ids)
150
+ console.print("done")
151
+
152
+ right_now = Timestamp()
153
+ total_size = sum(len(node_ids) for node_ids in node_ids_to_update_by_branch.values())
154
+ with Progress() as progress:
155
+ apply_task = progress.add_task("Applying profiles to nodes...", total=total_size)
156
+ for branch_name, node_ids in node_ids_to_update_by_branch.items():
157
+ applier = await self._get_profile_applier(db=db, branch_name=branch_name)
158
+ for node_id in node_ids:
159
+ node = await NodeManager.get_one(db=db, branch=branch_name, id=node_id, at=right_now)
160
+ if node:
161
+ updated_field_names = await applier.apply_profiles(node=node)
162
+ if updated_field_names:
163
+ await node.save(db=db, fields=updated_field_names, at=right_now)
164
+ progress.update(apply_task, advance=1)
165
+
166
+ return result
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from rich.progress import Progress
6
+ from typing_extensions import Self
7
+
8
+ from infrahub.core import registry
9
+ from infrahub.core.constants import SchemaPathType
10
+ from infrahub.core.initialization import initialization
11
+ from infrahub.core.migrations.schema.node_attribute_add import NodeAttributeAddMigration
12
+ from infrahub.core.migrations.shared import InternalSchemaMigration, MigrationResult
13
+ from infrahub.core.path import SchemaPath
14
+ from infrahub.lock import initialize_lock
15
+
16
+ if TYPE_CHECKING:
17
+ from infrahub.database import InfrahubDatabase
18
+
19
+
20
+ class Migration041(InternalSchemaMigration):
21
+ name: str = "041_create_hfid_display_label_in_db"
22
+ minimum_version: int = 40
23
+
24
+ @classmethod
25
+ def init(cls, **kwargs: Any) -> Self:
26
+ internal_schema = cls.get_internal_schema()
27
+ schema_node = internal_schema.get_node(name="SchemaNode")
28
+ schema_generic = internal_schema.get_node(name="SchemaGeneric")
29
+
30
+ cls.migrations = [
31
+ # HFID is not needed, it was introduced at graph v8
32
+ NodeAttributeAddMigration(
33
+ new_node_schema=schema_node,
34
+ previous_node_schema=schema_node,
35
+ schema_path=SchemaPath(
36
+ schema_kind="SchemaNode", path_type=SchemaPathType.ATTRIBUTE, field_name="display_label"
37
+ ),
38
+ ),
39
+ NodeAttributeAddMigration(
40
+ new_node_schema=schema_generic,
41
+ previous_node_schema=schema_generic,
42
+ schema_path=SchemaPath(
43
+ schema_kind="SchemaGeneric", path_type=SchemaPathType.ATTRIBUTE, field_name="display_label"
44
+ ),
45
+ ),
46
+ ]
47
+ return cls(migrations=cls.migrations, **kwargs) # type: ignore[arg-type]
48
+
49
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
50
+ result = MigrationResult()
51
+
52
+ # load schemas from database into registry
53
+ initialize_lock()
54
+ await initialization(db=db)
55
+
56
+ default_branch = registry.get_branch_from_registry()
57
+ schema_branch = await registry.schema.load_schema_from_db(db=db, branch=default_branch)
58
+
59
+ migrations = list(self.migrations)
60
+
61
+ for node_schema_kind in schema_branch.node_names:
62
+ schema = schema_branch.get(name=node_schema_kind, duplicate=False)
63
+ migrations.extend(
64
+ [
65
+ NodeAttributeAddMigration(
66
+ new_node_schema=schema,
67
+ previous_node_schema=schema,
68
+ schema_path=SchemaPath(
69
+ schema_kind=schema.kind, path_type=SchemaPathType.ATTRIBUTE, field_name="human_friendly_id"
70
+ ),
71
+ ),
72
+ NodeAttributeAddMigration(
73
+ new_node_schema=schema,
74
+ previous_node_schema=schema,
75
+ schema_path=SchemaPath(
76
+ schema_kind=schema.kind, path_type=SchemaPathType.ATTRIBUTE, field_name="display_label"
77
+ ),
78
+ ),
79
+ ]
80
+ )
81
+
82
+ with Progress() as progress:
83
+ update_task = progress.add_task("Adding HFID and display label to nodes", total=len(migrations))
84
+
85
+ for migration in migrations:
86
+ try:
87
+ execution_result = await migration.execute(db=db, branch=default_branch)
88
+ result.errors.extend(execution_result.errors)
89
+ progress.update(update_task, advance=1)
90
+ except Exception as exc:
91
+ result.errors.append(str(exc))
92
+ return result
93
+
94
+ return result
95
+
96
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
97
+ return MigrationResult()
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from rich.progress import Progress, TaskID
6
+
7
+ from infrahub.core import registry
8
+ from infrahub.core.initialization import initialization
9
+ from infrahub.core.manager import NodeManager
10
+ from infrahub.core.migrations.shared import MigrationResult
11
+ from infrahub.lock import initialize_lock
12
+
13
+ from ..shared import ArbitraryMigration
14
+
15
+ if TYPE_CHECKING:
16
+ from infrahub.core.node import Node
17
+ from infrahub.core.schema import MainSchemaTypes
18
+ from infrahub.database import InfrahubDatabase
19
+
20
+
21
+ class Migration042(ArbitraryMigration):
22
+ """
23
+ Backfill `human_friendly_id` and `display_label` attributes for nodes with schemas that define them.
24
+ """
25
+
26
+ name: str = "042_backfill_hfid_display_label_in_db"
27
+ minimum_version: int = 41
28
+
29
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
30
+ super().__init__(*args, **kwargs)
31
+
32
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
33
+ return MigrationResult()
34
+
35
+ async def _update_batch(
36
+ self,
37
+ db: InfrahubDatabase,
38
+ node_schema: MainSchemaTypes,
39
+ nodes: Sequence[Node],
40
+ progress: Progress,
41
+ update_task: TaskID,
42
+ ) -> None:
43
+ for node in nodes:
44
+ fields = []
45
+ if node_schema.human_friendly_id:
46
+ await node.add_human_friendly_id(db=db)
47
+ fields.append("human_friendly_id")
48
+ if node_schema.display_label:
49
+ await node.add_display_label(db=db)
50
+ fields.append("display_label")
51
+
52
+ if fields:
53
+ await node.save(db=db, fields=fields)
54
+
55
+ progress.update(task_id=update_task, advance=1)
56
+
57
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
58
+ result = MigrationResult()
59
+ # load schemas from database into registry
60
+ initialize_lock()
61
+ await initialization(db=db)
62
+
63
+ schemas_to_update: dict[MainSchemaTypes, int] = {}
64
+ for node_schema in registry.get_full_schema(duplicate=False).values():
65
+ if node_schema.is_generic_schema or (not node_schema.human_friendly_id and not node_schema.display_label):
66
+ continue
67
+
68
+ node_count = await NodeManager.count(db=db, schema=node_schema)
69
+ if node_count:
70
+ schemas_to_update[node_schema] = node_count
71
+
72
+ with Progress() as progress:
73
+ batch_size = 1000
74
+ update_task = progress.add_task(
75
+ "Backfill HFID and display_label for nodes in database", total=sum(schemas_to_update.values())
76
+ )
77
+
78
+ for schema, count in schemas_to_update.items():
79
+ for offset in range(0, count, batch_size):
80
+ limit = min(batch_size, count - offset)
81
+ nodes: list[Node] = await NodeManager.query(db=db, schema=schema, offset=offset, limit=limit)
82
+ await self._update_batch(
83
+ db=db, node_schema=schema, nodes=nodes, progress=progress, update_task=update_task
84
+ )
85
+
86
+ return result
@@ -52,8 +52,11 @@ class NodeAttributeAddMigration(AttributeSchemaMigration):
52
52
  if self.new_attribute_schema.kind != "NumberPool":
53
53
  return result
54
54
 
55
- number_pool: CoreNumberPool = await Node.fetch_or_create_number_pool( # type: ignore[assignment]
56
- db=db, branch=branch, schema_node=self.new_schema, schema_attribute=self.new_attribute_schema
55
+ number_pool: CoreNumberPool = await Node.fetch_or_create_number_pool(
56
+ db=db,
57
+ branch=branch,
58
+ schema_node=self.new_schema, # type: ignore
59
+ schema_attribute=self.new_attribute_schema,
57
60
  )
58
61
 
59
62
  await update_branch_registry(db=db, branch=branch)
@@ -10,8 +10,7 @@ from infrahub.core.path import SchemaPath # noqa: TC001
10
10
  from infrahub.core.query import Query # noqa: TC001
11
11
  from infrahub.core.schema import (
12
12
  AttributeSchema,
13
- GenericSchema,
14
- NodeSchema,
13
+ MainSchemaTypes,
15
14
  RelationshipSchema,
16
15
  SchemaRoot,
17
16
  internal_schema,
@@ -43,8 +42,8 @@ class SchemaMigration(BaseModel):
43
42
  name: str = Field(..., description="Name of the migration")
44
43
  queries: Sequence[type[MigrationQuery]] = Field(..., description="List of queries to execute for this migration")
45
44
 
46
- new_node_schema: NodeSchema | GenericSchema | None = None
47
- previous_node_schema: NodeSchema | GenericSchema | None = None
45
+ new_node_schema: MainSchemaTypes | None = None
46
+ previous_node_schema: MainSchemaTypes | None = None
48
47
  schema_path: SchemaPath
49
48
 
50
49
  async def execute_pre_queries(
@@ -91,13 +90,13 @@ class SchemaMigration(BaseModel):
91
90
  return result
92
91
 
93
92
  @property
94
- def new_schema(self) -> NodeSchema | GenericSchema:
93
+ def new_schema(self) -> MainSchemaTypes:
95
94
  if self.new_node_schema:
96
95
  return self.new_node_schema
97
96
  raise ValueError("new_node_schema hasn't been initialized")
98
97
 
99
98
  @property
100
- def previous_schema(self) -> NodeSchema | GenericSchema:
99
+ def previous_schema(self) -> MainSchemaTypes:
101
100
  if self.previous_node_schema:
102
101
  return self.previous_node_schema
103
102
  raise ValueError("previous_node_schema hasn't been initialized")