infrahub-server 1.5.0b0__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 (104) hide show
  1. infrahub/actions/tasks.py +8 -0
  2. infrahub/api/diff/diff.py +1 -1
  3. infrahub/cli/db.py +24 -0
  4. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  5. infrahub/core/attribute.py +3 -3
  6. infrahub/core/branch/tasks.py +2 -1
  7. infrahub/core/changelog/models.py +4 -12
  8. infrahub/core/constants/infrahubkind.py +1 -0
  9. infrahub/core/diff/model/path.py +4 -0
  10. infrahub/core/diff/payload_builder.py +1 -1
  11. infrahub/core/graph/__init__.py +1 -1
  12. infrahub/core/ipam/utilization.py +1 -1
  13. infrahub/core/manager.py +6 -3
  14. infrahub/core/migrations/graph/__init__.py +4 -0
  15. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
  16. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
  17. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  18. infrahub/core/migrations/shared.py +5 -6
  19. infrahub/core/node/__init__.py +142 -40
  20. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  21. infrahub/core/node/node_property_attribute.py +230 -0
  22. infrahub/core/node/standard.py +1 -1
  23. infrahub/core/protocols.py +7 -1
  24. infrahub/core/query/node.py +14 -1
  25. infrahub/core/registry.py +2 -2
  26. infrahub/core/relationship/constraints/count.py +1 -1
  27. infrahub/core/relationship/model.py +1 -1
  28. infrahub/core/schema/basenode_schema.py +42 -2
  29. infrahub/core/schema/definitions/core/__init__.py +2 -0
  30. infrahub/core/schema/definitions/core/generator.py +2 -0
  31. infrahub/core/schema/definitions/core/group.py +16 -2
  32. infrahub/core/schema/definitions/internal.py +14 -1
  33. infrahub/core/schema/generated/base_node_schema.py +6 -1
  34. infrahub/core/schema/node_schema.py +5 -2
  35. infrahub/core/schema/schema_branch.py +134 -0
  36. infrahub/core/schema/schema_branch_display.py +123 -0
  37. infrahub/core/schema/schema_branch_hfid.py +114 -0
  38. infrahub/core/validators/aggregated_checker.py +1 -1
  39. infrahub/core/validators/determiner.py +12 -1
  40. infrahub/core/validators/relationship/peer.py +1 -1
  41. infrahub/core/validators/tasks.py +1 -1
  42. infrahub/display_labels/__init__.py +0 -0
  43. infrahub/display_labels/gather.py +48 -0
  44. infrahub/display_labels/models.py +240 -0
  45. infrahub/display_labels/tasks.py +186 -0
  46. infrahub/display_labels/triggers.py +22 -0
  47. infrahub/events/group_action.py +1 -1
  48. infrahub/events/node_action.py +1 -1
  49. infrahub/generators/constants.py +7 -0
  50. infrahub/generators/models.py +7 -0
  51. infrahub/generators/tasks.py +31 -15
  52. infrahub/git/integrator.py +22 -14
  53. infrahub/graphql/analyzer.py +1 -1
  54. infrahub/graphql/mutations/display_label.py +111 -0
  55. infrahub/graphql/mutations/generator.py +25 -7
  56. infrahub/graphql/mutations/hfid.py +118 -0
  57. infrahub/graphql/mutations/relationship.py +2 -2
  58. infrahub/graphql/mutations/resource_manager.py +2 -2
  59. infrahub/graphql/mutations/schema.py +5 -5
  60. infrahub/graphql/queries/resource_manager.py +1 -1
  61. infrahub/graphql/resolvers/resolver.py +2 -0
  62. infrahub/graphql/schema.py +4 -0
  63. infrahub/groups/tasks.py +1 -1
  64. infrahub/hfid/__init__.py +0 -0
  65. infrahub/hfid/gather.py +48 -0
  66. infrahub/hfid/models.py +240 -0
  67. infrahub/hfid/tasks.py +185 -0
  68. infrahub/hfid/triggers.py +22 -0
  69. infrahub/lock.py +15 -4
  70. infrahub/middleware.py +26 -1
  71. infrahub/proposed_change/tasks.py +10 -1
  72. infrahub/server.py +16 -3
  73. infrahub/services/__init__.py +8 -5
  74. infrahub/trigger/catalogue.py +4 -0
  75. infrahub/trigger/models.py +2 -0
  76. infrahub/trigger/tasks.py +3 -0
  77. infrahub/workflows/catalogue.py +72 -0
  78. infrahub/workflows/initialization.py +16 -0
  79. infrahub_sdk/checks.py +1 -1
  80. infrahub_sdk/ctl/cli_commands.py +2 -0
  81. infrahub_sdk/ctl/generator.py +4 -0
  82. infrahub_sdk/ctl/graphql.py +184 -0
  83. infrahub_sdk/ctl/schema.py +6 -2
  84. infrahub_sdk/generator.py +7 -1
  85. infrahub_sdk/graphql/__init__.py +12 -0
  86. infrahub_sdk/graphql/constants.py +1 -0
  87. infrahub_sdk/graphql/plugin.py +85 -0
  88. infrahub_sdk/graphql/query.py +77 -0
  89. infrahub_sdk/{graphql.py → graphql/renderers.py} +81 -73
  90. infrahub_sdk/graphql/utils.py +40 -0
  91. infrahub_sdk/protocols.py +14 -0
  92. infrahub_sdk/schema/__init__.py +38 -0
  93. infrahub_sdk/schema/repository.py +8 -0
  94. infrahub_sdk/spec/object.py +84 -10
  95. infrahub_sdk/spec/range_expansion.py +1 -1
  96. infrahub_sdk/transforms.py +1 -1
  97. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +5 -4
  98. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +104 -79
  99. infrahub_testcontainers/container.py +1 -1
  100. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  101. infrahub_testcontainers/docker-compose.test.yml +1 -1
  102. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
  103. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
  104. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
infrahub/actions/tasks.py CHANGED
@@ -61,6 +61,12 @@ def get_generator_run_query(definition_id: str, target_ids: list[str]) -> Query:
61
61
  "parameters": {
62
62
  "value": None,
63
63
  },
64
+ "execute_in_proposed_change": {
65
+ "value": None,
66
+ },
67
+ "execute_after_merge": {
68
+ "value": None,
69
+ },
64
70
  "targets": {
65
71
  "node": {
66
72
  "id": None,
@@ -287,6 +293,8 @@ async def _run_generators(
287
293
  convert_query_response=data["convert_query_response"]["value"],
288
294
  group_id=data["targets"]["node"]["id"],
289
295
  parameters=data["parameters"]["value"],
296
+ execute_in_proposed_change=data["execute_in_proposed_change"]["value"],
297
+ execute_after_merge=data["execute_after_merge"]["value"],
290
298
  ),
291
299
  commit=data["repository"]["node"]["commit"]["value"],
292
300
  repository_id=data["repository"]["node"]["id"],
infrahub/api/diff/diff.py CHANGED
@@ -52,7 +52,7 @@ async def get_diff_files(
52
52
  for branch_name, items in diff_files.items():
53
53
  for item in items:
54
54
  repository_id = item.repository.get_id()
55
- display_label = await item.repository.render_display_label(db=db)
55
+ display_label = await item.repository.get_display_label(db=db)
56
56
  if repository_id not in response[branch_name]:
57
57
  response[branch_name][repository_id] = BranchDiffRepository(
58
58
  id=repository_id,
infrahub/cli/db.py CHANGED
@@ -54,6 +54,7 @@ from infrahub.log import get_logger
54
54
 
55
55
  from .constants import ERROR_BADGE, FAILED_BADGE, SUCCESS_BADGE
56
56
  from .db_commands.check_inheritance import check_inheritance
57
+ from .db_commands.clean_duplicate_schema_fields import clean_duplicate_schema_fields
57
58
  from .patch import patch_app
58
59
 
59
60
 
@@ -200,6 +201,29 @@ async def check_inheritance_cmd(
200
201
  await dbdriver.close()
201
202
 
202
203
 
204
+ @app.command(name="check-duplicate-schema-fields")
205
+ async def check_duplicate_schema_fields_cmd(
206
+ ctx: typer.Context,
207
+ fix: bool = typer.Option(False, help="Fix the duplicate schema fields on the default branch."),
208
+ config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
209
+ ) -> None:
210
+ """Check for any duplicate schema attributes or relationships on the default branch"""
211
+ logging.getLogger("infrahub").setLevel(logging.WARNING)
212
+ logging.getLogger("neo4j").setLevel(logging.ERROR)
213
+ logging.getLogger("prefect").setLevel(logging.ERROR)
214
+
215
+ config.load_and_exit(config_file_name=config_file)
216
+
217
+ context: CliContext = ctx.obj
218
+ dbdriver = await context.init_db(retry=1)
219
+
220
+ success = await clean_duplicate_schema_fields(db=dbdriver, fix=fix)
221
+ if not success:
222
+ raise typer.Exit(code=1)
223
+
224
+ await dbdriver.close()
225
+
226
+
203
227
  @app.command(name="update-core-schema")
204
228
  async def update_core_schema_cmd(
205
229
  ctx: typer.Context,
@@ -0,0 +1,212 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Any
4
+
5
+ from rich import print as rprint
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from infrahub.cli.constants import FAILED_BADGE, SUCCESS_BADGE
10
+ from infrahub.core.query import Query, QueryType
11
+ from infrahub.database import InfrahubDatabase
12
+
13
+
14
+ class SchemaFieldType(str, Enum):
15
+ ATTRIBUTE = "attribute"
16
+ RELATIONSHIP = "relationship"
17
+
18
+
19
+ @dataclass
20
+ class SchemaFieldDetails:
21
+ schema_kind: str
22
+ schema_uuid: str
23
+ field_type: SchemaFieldType
24
+ field_name: str
25
+
26
+
27
+ class DuplicateSchemaFields(Query):
28
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
29
+ query = """
30
+ MATCH (root:Root)
31
+ LIMIT 1
32
+ WITH root.default_branch AS default_branch
33
+ MATCH (field:SchemaAttribute|SchemaRelationship)
34
+ CALL (default_branch, field) {
35
+ MATCH (field)-[is_part_of:IS_PART_OF]->(:Root)
36
+ WHERE is_part_of.branch = default_branch
37
+ ORDER BY is_part_of.from DESC
38
+ RETURN is_part_of
39
+ LIMIT 1
40
+ }
41
+ WITH default_branch, field, CASE
42
+ WHEN is_part_of.status = "active" AND is_part_of.to IS NULL THEN is_part_of.from
43
+ ELSE NULL
44
+ END AS active_from
45
+ WHERE active_from IS NOT NULL
46
+ WITH default_branch, field, active_from, "SchemaAttribute" IN labels(field) AS is_attribute
47
+ CALL (field, default_branch) {
48
+ MATCH (field)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "name"})-[r2:HAS_VALUE]->(name_value:AttributeValue)
49
+ WHERE r1.branch = default_branch AND r2.branch = default_branch
50
+ AND r1.status = "active" AND r2.status = "active"
51
+ AND r1.to IS NULL AND r2.to IS NULL
52
+ ORDER BY r1.from DESC, r1.status ASC, r2.from DESC, r2.status ASC
53
+ LIMIT 1
54
+ RETURN name_value.value AS field_name
55
+ }
56
+ CALL (field, default_branch) {
57
+ MATCH (field)-[r1:IS_RELATED]-(rel:Relationship)-[r2:IS_RELATED]-(peer:SchemaNode|SchemaGeneric)
58
+ WHERE rel.name IN ["schema__node__relationships", "schema__node__attributes"]
59
+ AND r1.branch = default_branch AND r2.branch = default_branch
60
+ AND r1.status = "active" AND r2.status = "active"
61
+ AND r1.to IS NULL AND r2.to IS NULL
62
+ ORDER BY r1.from DESC, r1.status ASC, r2.from DESC, r2.status ASC
63
+ LIMIT 1
64
+ RETURN peer AS schema_vertex
65
+ }
66
+ WITH default_branch, field, field_name, is_attribute, active_from, schema_vertex
67
+ ORDER BY active_from DESC
68
+ WITH default_branch, field_name, is_attribute, schema_vertex, collect(field) AS fields_reverse_chron
69
+ WHERE size(fields_reverse_chron) > 1
70
+ """
71
+ self.add_to_query(query)
72
+
73
+
74
+ class GetDuplicateSchemaFields(DuplicateSchemaFields):
75
+ """
76
+ Get the kind, field type, and field name for any duplicated attributes or relationships on a given schema
77
+ on the default branch
78
+ """
79
+
80
+ name = "get_duplicate_schema_fields"
81
+ type = QueryType.READ
82
+ insert_return = False
83
+
84
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
85
+ await super().query_init(db=db, **kwargs)
86
+ query = """
87
+ CALL (schema_vertex, default_branch) {
88
+ MATCH (schema_vertex)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "namespace"})-[r2:HAS_VALUE]->(name_value:AttributeValue)
89
+ WHERE r1.branch = default_branch AND r2.branch = default_branch
90
+ ORDER BY r1.from DESC, r1.status ASC, r2.from DESC, r2.status ASC
91
+ LIMIT 1
92
+ RETURN name_value.value AS schema_namespace
93
+ }
94
+ CALL (schema_vertex, default_branch) {
95
+ MATCH (schema_vertex)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "name"})-[r2:HAS_VALUE]->(name_value:AttributeValue)
96
+ WHERE r1.branch = default_branch AND r2.branch = default_branch
97
+ ORDER BY r1.from DESC, r1.status ASC, r2.from DESC, r2.status ASC
98
+ LIMIT 1
99
+ RETURN name_value.value AS schema_name
100
+ }
101
+ RETURN schema_namespace + schema_name AS schema_kind, schema_vertex.uuid AS schema_uuid, field_name, is_attribute
102
+ ORDER BY schema_kind ASC, is_attribute DESC, field_name ASC
103
+ """
104
+ self.return_labels = ["schema_kind", "schema_uuid", "field_name", "is_attribute"]
105
+ self.add_to_query(query)
106
+
107
+ def get_schema_field_details(self) -> list[SchemaFieldDetails]:
108
+ schema_field_details: list[SchemaFieldDetails] = []
109
+ for result in self.results:
110
+ schema_kind = result.get_as_type(label="schema_kind", return_type=str)
111
+ schema_uuid = result.get_as_type(label="schema_uuid", return_type=str)
112
+ field_name = result.get_as_type(label="field_name", return_type=str)
113
+ is_attribute = result.get_as_type(label="is_attribute", return_type=bool)
114
+ schema_field_details.append(
115
+ SchemaFieldDetails(
116
+ schema_kind=schema_kind,
117
+ schema_uuid=schema_uuid,
118
+ field_name=field_name,
119
+ field_type=SchemaFieldType.ATTRIBUTE if is_attribute else SchemaFieldType.RELATIONSHIP,
120
+ )
121
+ )
122
+ return schema_field_details
123
+
124
+
125
+ class FixDuplicateSchemaFields(DuplicateSchemaFields):
126
+ """
127
+ Fix the duplicate schema fields by hard deleting the earlier duplicate(s)
128
+ """
129
+
130
+ name = "fix_duplicate_schema_fields"
131
+ type = QueryType.WRITE
132
+ insert_return = False
133
+
134
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
135
+ await super().query_init(db=db, **kwargs)
136
+ query = """
137
+ WITH default_branch, tail(fields_reverse_chron) AS fields_to_delete
138
+ UNWIND fields_to_delete AS field_to_delete
139
+ CALL (field_to_delete, default_branch) {
140
+ MATCH (field_to_delete)-[r:IS_PART_OF {branch: default_branch}]-()
141
+ DELETE r
142
+ WITH field_to_delete
143
+ MATCH (field_to_delete)-[:IS_RELATED {branch: default_branch}]-(rel:Relationship)
144
+ WITH DISTINCT field_to_delete, rel
145
+ MATCH (rel)-[r {branch: default_branch}]-()
146
+ DELETE r
147
+ WITH field_to_delete, rel
148
+ OPTIONAL MATCH (rel)
149
+ WHERE NOT exists((rel)--())
150
+ DELETE rel
151
+ WITH DISTINCT field_to_delete
152
+ MATCH (field_to_delete)-[:HAS_ATTRIBUTE {branch: default_branch}]->(attr:Attribute)
153
+ MATCH (attr)-[r {branch: default_branch}]-()
154
+ DELETE r
155
+ WITH field_to_delete, attr
156
+ OPTIONAL MATCH (attr)
157
+ WHERE NOT exists((attr)--())
158
+ DELETE attr
159
+ WITH DISTINCT field_to_delete
160
+ OPTIONAL MATCH (field_to_delete)
161
+ WHERE NOT exists((field_to_delete)--())
162
+ DELETE field_to_delete
163
+ }
164
+ """
165
+ self.add_to_query(query)
166
+
167
+
168
+ def display_duplicate_schema_fields(duplicate_schema_fields: list[SchemaFieldDetails]) -> None:
169
+ console = Console()
170
+
171
+ table = Table(title="Duplicate Schema Fields on Default Branch")
172
+
173
+ table.add_column("Schema Kind")
174
+ table.add_column("Schema UUID")
175
+ table.add_column("Field Name")
176
+ table.add_column("Field Type")
177
+
178
+ for duplicate_schema_field in duplicate_schema_fields:
179
+ table.add_row(
180
+ duplicate_schema_field.schema_kind,
181
+ duplicate_schema_field.schema_uuid,
182
+ duplicate_schema_field.field_name,
183
+ duplicate_schema_field.field_type.value,
184
+ )
185
+
186
+ console.print(table)
187
+
188
+
189
+ async def clean_duplicate_schema_fields(db: InfrahubDatabase, fix: bool = False) -> bool:
190
+ """
191
+ Identify any attributes or relationships that are duplicated in a schema on the default branch
192
+ If fix is True, runs cypher queries to hard delete the earlier duplicate
193
+ """
194
+
195
+ duplicate_schema_fields_query = await GetDuplicateSchemaFields.init(db=db)
196
+ await duplicate_schema_fields_query.execute(db=db)
197
+ duplicate_schema_fields = duplicate_schema_fields_query.get_schema_field_details()
198
+
199
+ if not duplicate_schema_fields:
200
+ rprint(f"{SUCCESS_BADGE} No duplicate schema fields found")
201
+ return True
202
+
203
+ display_duplicate_schema_fields(duplicate_schema_fields)
204
+
205
+ if not fix:
206
+ rprint(f"{FAILED_BADGE} Use the --fix flag to fix the duplicate schema fields")
207
+ return False
208
+
209
+ fix_duplicate_schema_fields_query = await FixDuplicateSchemaFields.init(db=db)
210
+ await fix_duplicate_schema_fields_query.execute(db=db)
211
+ rprint(f"{SUCCESS_BADGE} Duplicate schema fields deleted from the default branch")
212
+ return True
@@ -36,7 +36,7 @@ from .schema.attribute_parameters import NumberAttributeParameters
36
36
  if TYPE_CHECKING:
37
37
  from infrahub.core.branch import Branch
38
38
  from infrahub.core.node import Node
39
- from infrahub.core.schema import AttributeSchema
39
+ from infrahub.core.schema import AttributeSchema, MainSchemaTypes
40
40
  from infrahub.database import InfrahubDatabase
41
41
 
42
42
 
@@ -630,7 +630,7 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
630
630
  return AttributeDBNodeType.DEFAULT
631
631
  return AttributeDBNodeType.INDEXED
632
632
 
633
- def get_create_data(self) -> AttributeCreateData:
633
+ def get_create_data(self, node_schema: MainSchemaTypes) -> AttributeCreateData:
634
634
  branch = self.branch
635
635
  hierarchy_level = branch.hierarchy_level
636
636
  if self.schema.branch == BranchSupportType.AGNOSTIC:
@@ -645,7 +645,7 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
645
645
  branch=branch.name,
646
646
  status="active",
647
647
  branch_level=hierarchy_level,
648
- branch_support=self.schema.branch.value,
648
+ branch_support=self.schema.branch.value if self.schema.branch is not None else node_schema.branch,
649
649
  content=self.to_db(),
650
650
  is_default=self.is_default,
651
651
  is_protected=self.is_protected,
@@ -33,6 +33,7 @@ from infrahub.events.branch_action import BranchCreatedEvent, BranchDeletedEvent
33
33
  from infrahub.events.models import EventMeta, InfrahubEvent
34
34
  from infrahub.events.node_action import get_node_event
35
35
  from infrahub.exceptions import BranchNotFoundError, ValidationError
36
+ from infrahub.generators.constants import GeneratorDefinitionRunSource
36
37
  from infrahub.graphql.mutations.models import BranchCreateModel # noqa: TC001
37
38
  from infrahub.workers.dependencies import get_component, get_database, get_event_service, get_workflow
38
39
  from infrahub.workflows.catalogue import (
@@ -437,7 +438,7 @@ async def post_process_branch_merge(source_branch: str, target_branch: str, cont
437
438
  await get_workflow().submit_workflow(
438
439
  workflow=TRIGGER_GENERATOR_DEFINITION_RUN,
439
440
  context=context,
440
- parameters={"branch": target_branch},
441
+ parameters={"branch": target_branch, "source": GeneratorDefinitionRunSource.MERGE},
441
442
  )
442
443
 
443
444
  for diff_root in branch_diff_roots:
@@ -560,7 +560,7 @@ class RelationshipChangelogGetter:
560
560
 
561
561
  for peer in relationship.peers:
562
562
  if peer.peer_status == DiffAction.ADDED:
563
- peer_schema = schema_branch.get(name=peer.peer_kind)
563
+ peer_schema = schema_branch.get(name=peer.peer_kind, duplicate=False)
564
564
  secondaries.extend(
565
565
  self._process_added_peers(
566
566
  peer_id=peer.peer_id,
@@ -572,7 +572,7 @@ class RelationshipChangelogGetter:
572
572
  )
573
573
 
574
574
  elif peer.peer_status == DiffAction.REMOVED:
575
- peer_schema = schema_branch.get(name=peer.peer_kind)
575
+ peer_schema = schema_branch.get(name=peer.peer_kind, duplicate=False)
576
576
  secondaries.extend(
577
577
  self._process_removed_peers(
578
578
  peer_id=peer.peer_id,
@@ -596,11 +596,7 @@ class RelationshipChangelogGetter:
596
596
  secondaries: list[NodeChangelog] = []
597
597
  peer_relation = peer_schema.get_relationship_by_identifier(id=str(rel_schema.identifier), raise_on_error=False)
598
598
  if peer_relation:
599
- node_changelog = NodeChangelog(
600
- node_id=peer_id,
601
- node_kind=peer_kind,
602
- display_label="n/a",
603
- )
599
+ node_changelog = NodeChangelog(node_id=peer_id, node_kind=peer_kind, display_label="n/a")
604
600
  if peer_relation.cardinality == RelationshipCardinality.ONE:
605
601
  node_changelog.relationships[peer_relation.name] = RelationshipCardinalityOneChangelog(
606
602
  name=peer_relation.name,
@@ -634,11 +630,7 @@ class RelationshipChangelogGetter:
634
630
  secondaries: list[NodeChangelog] = []
635
631
  peer_relation = peer_schema.get_relationship_by_identifier(id=str(rel_schema.identifier), raise_on_error=False)
636
632
  if peer_relation:
637
- node_changelog = NodeChangelog(
638
- node_id=peer_id,
639
- node_kind=peer_kind,
640
- display_label="n/a",
641
- )
633
+ node_changelog = NodeChangelog(node_id=peer_id, node_kind=peer_kind, display_label="n/a")
642
634
  if peer_relation.cardinality == RelationshipCardinality.ONE:
643
635
  node_changelog.relationships[peer_relation.name] = RelationshipCardinalityOneChangelog(
644
636
  name=peer_relation.name,
@@ -28,6 +28,7 @@ GENERATORDEFINITION = "CoreGeneratorDefinition"
28
28
  GENERATORINSTANCE = "CoreGeneratorInstance"
29
29
  GENERATORVALIDATOR = "CoreGeneratorValidator"
30
30
  GENERATORGROUP = "CoreGeneratorGroup"
31
+ GENERATORAWAREGROUP = "CoreGeneratorAwareGroup"
31
32
  GENERICGROUP = "CoreGroup"
32
33
  GLOBALPERMISSION = "CoreGlobalPermission"
33
34
  GRAPHQLQUERY = "CoreGraphQLQuery"
@@ -335,6 +335,10 @@ class EnrichedDiffNode(BaseSummary):
335
335
  def kind(self) -> str:
336
336
  return self.identifier.kind
337
337
 
338
+ @property
339
+ def is_schema_node(self) -> bool:
340
+ return self.identifier.kind.startswith("Schema")
341
+
338
342
  @property
339
343
  def num_properties(self) -> int:
340
344
  return sum(a.num_properties for a in self.attributes) + sum(r.num_properties for r in self.relationships)
@@ -36,7 +36,7 @@ async def get_display_labels_per_kind(
36
36
  break
37
37
  node_map = await NodeManager.get_many(ids=limited_ids, fields=fields, db=db, branch=branch)
38
38
  for node_id, node in node_map.items():
39
- display_label_map[node_id] = await node.render_display_label(db=db)
39
+ display_label_map[node_id] = await node.get_display_label(db=db)
40
40
  offset += limit
41
41
  return display_label_map
42
42
 
@@ -1 +1 @@
1
- GRAPH_VERSION = 40
1
+ GRAPH_VERSION = 42
@@ -152,4 +152,4 @@ class PrefixUtilizationGetter:
152
152
  grand_total_space += prefix_total_space
153
153
  if grand_total_space == 0:
154
154
  return 0.0
155
- return (grand_total_used / grand_total_space) * 100
155
+ return min((grand_total_used / grand_total_space) * 100, 100)
infrahub/core/manager.py CHANGED
@@ -60,12 +60,15 @@ def identify_node_class(node: NodeToProcess) -> type[Node]:
60
60
 
61
61
 
62
62
  def get_schema(
63
- 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,
64
67
  ) -> MainSchemaTypes:
65
68
  if isinstance(node_schema, str):
66
- return db.schema.get(name=node_schema, branch=branch.name)
69
+ return db.schema.get(name=node_schema, branch=branch.name, duplicate=duplicate)
67
70
  if hasattr(node_schema, "_is_runtime_protocol") and node_schema._is_runtime_protocol:
68
- 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)
69
72
  if not isinstance(node_schema, (MainSchemaTypes)):
70
73
  raise ValueError(f"Invalid schema provided {node_schema}")
71
74
 
@@ -42,6 +42,8 @@ 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
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
45
47
 
46
48
  if TYPE_CHECKING:
47
49
  from infrahub.core.root import Root
@@ -89,6 +91,8 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
89
91
  Migration038,
90
92
  Migration039,
91
93
  Migration040,
94
+ Migration041,
95
+ Migration042,
92
96
  ]
93
97
 
94
98
 
@@ -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)