infrahub-server 1.6.0b0__py3-none-any.whl → 1.6.1__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 (88) hide show
  1. infrahub/api/oauth2.py +33 -6
  2. infrahub/api/oidc.py +36 -6
  3. infrahub/auth.py +11 -0
  4. infrahub/auth_pkce.py +41 -0
  5. infrahub/config.py +9 -3
  6. infrahub/core/branch/models.py +3 -2
  7. infrahub/core/changelog/models.py +2 -2
  8. infrahub/core/constants/__init__.py +1 -0
  9. infrahub/core/graph/__init__.py +1 -1
  10. infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
  11. infrahub/core/manager.py +36 -31
  12. infrahub/core/migrations/graph/__init__.py +2 -0
  13. infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +606 -0
  14. infrahub/core/models.py +5 -6
  15. infrahub/core/node/__init__.py +16 -13
  16. infrahub/core/node/create.py +36 -8
  17. infrahub/core/node/proposed_change.py +5 -3
  18. infrahub/core/node/standard.py +1 -1
  19. infrahub/core/protocols.py +1 -7
  20. infrahub/core/query/attribute.py +1 -1
  21. infrahub/core/query/node.py +9 -5
  22. infrahub/core/relationship/model.py +21 -4
  23. infrahub/core/schema/generic_schema.py +1 -1
  24. infrahub/core/schema/manager.py +8 -3
  25. infrahub/core/schema/schema_branch.py +35 -16
  26. infrahub/core/validators/attribute/choices.py +2 -2
  27. infrahub/core/validators/determiner.py +3 -6
  28. infrahub/database/__init__.py +1 -1
  29. infrahub/git/base.py +2 -3
  30. infrahub/git/models.py +13 -0
  31. infrahub/git/tasks.py +23 -19
  32. infrahub/git/utils.py +16 -9
  33. infrahub/graphql/app.py +6 -6
  34. infrahub/graphql/loaders/peers.py +6 -0
  35. infrahub/graphql/mutations/action.py +15 -7
  36. infrahub/graphql/mutations/hfid.py +1 -1
  37. infrahub/graphql/mutations/profile.py +8 -1
  38. infrahub/graphql/mutations/repository.py +3 -3
  39. infrahub/graphql/mutations/schema.py +4 -4
  40. infrahub/graphql/mutations/webhook.py +2 -2
  41. infrahub/graphql/queries/resource_manager.py +2 -3
  42. infrahub/graphql/queries/search.py +2 -3
  43. infrahub/graphql/resolvers/ipam.py +20 -0
  44. infrahub/graphql/resolvers/many_relationship.py +12 -11
  45. infrahub/graphql/resolvers/resolver.py +6 -2
  46. infrahub/graphql/resolvers/single_relationship.py +1 -11
  47. infrahub/log.py +1 -1
  48. infrahub/message_bus/messages/__init__.py +0 -12
  49. infrahub/profiles/node_applier.py +9 -0
  50. infrahub/proposed_change/branch_diff.py +1 -1
  51. infrahub/proposed_change/tasks.py +1 -1
  52. infrahub/repositories/create_repository.py +3 -3
  53. infrahub/task_manager/models.py +1 -1
  54. infrahub/task_manager/task.py +5 -5
  55. infrahub/trigger/setup.py +6 -9
  56. infrahub/utils.py +18 -0
  57. infrahub/validators/tasks.py +1 -1
  58. infrahub/workers/infrahub_async.py +7 -6
  59. infrahub_sdk/client.py +113 -1
  60. infrahub_sdk/ctl/AGENTS.md +67 -0
  61. infrahub_sdk/ctl/branch.py +175 -1
  62. infrahub_sdk/ctl/check.py +3 -3
  63. infrahub_sdk/ctl/cli_commands.py +9 -9
  64. infrahub_sdk/ctl/generator.py +2 -2
  65. infrahub_sdk/ctl/graphql.py +1 -2
  66. infrahub_sdk/ctl/importer.py +1 -2
  67. infrahub_sdk/ctl/repository.py +6 -49
  68. infrahub_sdk/ctl/task.py +2 -4
  69. infrahub_sdk/ctl/utils.py +2 -2
  70. infrahub_sdk/ctl/validate.py +1 -2
  71. infrahub_sdk/diff.py +80 -3
  72. infrahub_sdk/graphql/constants.py +14 -1
  73. infrahub_sdk/graphql/renderers.py +5 -1
  74. infrahub_sdk/node/attribute.py +0 -1
  75. infrahub_sdk/node/constants.py +3 -1
  76. infrahub_sdk/node/node.py +303 -3
  77. infrahub_sdk/node/related_node.py +1 -2
  78. infrahub_sdk/node/relationship.py +1 -2
  79. infrahub_sdk/protocols_base.py +0 -1
  80. infrahub_sdk/pytest_plugin/AGENTS.md +67 -0
  81. infrahub_sdk/schema/__init__.py +0 -3
  82. infrahub_sdk/timestamp.py +7 -7
  83. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/METADATA +2 -3
  84. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/RECORD +88 -84
  85. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/WHEEL +1 -1
  86. infrahub_testcontainers/container.py +2 -2
  87. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/entry_points.txt +0 -0
  88. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,606 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from infrahub_sdk.template import Jinja2Template
6
+ from rich.progress import Progress, TaskID
7
+
8
+ from infrahub.core import registry
9
+ from infrahub.core.branch import Branch
10
+ from infrahub.core.constants import GLOBAL_BRANCH_NAME, NULL_VALUE, BranchSupportType
11
+ from infrahub.core.initialization import get_root_node
12
+ from infrahub.core.migrations.shared import MigrationRequiringRebase, MigrationResult, get_migration_console
13
+ from infrahub.core.query import Query, QueryType
14
+
15
+ from .load_schema_branch import get_or_load_schema_branch
16
+ from .m044_backfill_hfid_display_label_in_db import (
17
+ DefaultBranchNodeCount,
18
+ GetPathDetailsBranchQuery,
19
+ GetPathDetailsDefaultBranch,
20
+ GetResultMapQuery,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from infrahub.core.schema import AttributeSchema, MainSchemaTypes
25
+ from infrahub.core.schema.basenode_schema import SchemaAttributePath
26
+ from infrahub.core.schema.schema_branch import SchemaBranch
27
+ from infrahub.database import InfrahubDatabase
28
+
29
+
30
+ console = get_migration_console()
31
+
32
+
33
+ def _is_jinja2_template(display_label: str) -> bool:
34
+ return any(c in display_label for c in "{}")
35
+
36
+
37
+ def _extract_jinja2_variables(template_str: str) -> list[str]:
38
+ return Jinja2Template(template=template_str).get_variables()
39
+
40
+
41
+ async def _render_display_label(display_label: str, variable_names: list[str], values: list[Any]) -> str | None:
42
+ if not _is_jinja2_template(display_label):
43
+ return values[0] if values and values[0] is not None else None
44
+
45
+ variables = dict(zip(variable_names, values, strict=False))
46
+ jinja_template = Jinja2Template(template=display_label)
47
+ return await jinja_template.render(variables=variables)
48
+
49
+
50
+ class UpdateAttributeValuesQuery(Query):
51
+ """
52
+ Update the values of the given attribute schema for the input node-id-to-value map.
53
+
54
+ This version only expires existing values when they're different from the new value,
55
+ making it safe to run idempotently without clearing correct existing values.
56
+
57
+ This code is adapted from m044_backfill_hfid_display_label_in_db.
58
+ """
59
+
60
+ name = "update_attribute_values"
61
+ type = QueryType.WRITE
62
+ insert_return = False
63
+
64
+ def __init__(self, attribute_schema: AttributeSchema, values_by_id_map: dict[str, Any], **kwargs: Any) -> None:
65
+ super().__init__(**kwargs)
66
+ self.attribute_name = attribute_schema.name
67
+ self.is_branch_agnostic = attribute_schema.get_branch() is BranchSupportType.AGNOSTIC
68
+ self.values_by_id_map = values_by_id_map
69
+
70
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
71
+ self.params = {
72
+ "node_uuids": list(self.values_by_id_map.keys()),
73
+ "attribute_name": self.attribute_name,
74
+ "values_by_id": self.values_by_id_map,
75
+ "default_branch": registry.default_branch,
76
+ "global_branch": GLOBAL_BRANCH_NAME,
77
+ "branch": GLOBAL_BRANCH_NAME if self.is_branch_agnostic else self.branch.name,
78
+ "branch_level": 1 if self.is_branch_agnostic else self.branch.hierarchy_level,
79
+ "at": self.at.to_string(),
80
+ }
81
+ branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
82
+ self.params.update(branch_filter_params)
83
+
84
+ if self.branch.name in [registry.default_branch, GLOBAL_BRANCH_NAME]:
85
+ update_value_query = """
86
+ // ------------
87
+ // Find the Nodes and Attributes we need to update
88
+ // ------------
89
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
90
+ WHERE n.uuid IN $node_uuids
91
+ AND e.branch IN [$default_branch, $global_branch]
92
+ AND e.to IS NULL
93
+ AND e.status = "active"
94
+ WITH DISTINCT n
95
+ MATCH (n)-[e:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
96
+ WHERE e.branch IN [$default_branch, $global_branch]
97
+ AND e.to IS NULL
98
+ AND e.status = "active"
99
+ // ------------
100
+ // If the attribute has an existing value on the branch, then set the to time on it
101
+ // but only if the value is different from the new value
102
+ // ------------
103
+ WITH DISTINCT n, attr
104
+ CALL (attr) {
105
+ OPTIONAL MATCH (attr)-[e:HAS_VALUE]->(existing_av)
106
+ WHERE e.branch IN [$default_branch, $global_branch]
107
+ AND e.to IS NULL
108
+ AND e.status = "active"
109
+ RETURN existing_av, e AS existing_has_value
110
+ }
111
+ CALL (existing_has_value, existing_av, n) {
112
+ WITH existing_has_value, existing_av, n
113
+ WHERE existing_has_value IS NOT NULL
114
+ AND existing_av.value <> $values_by_id[n.uuid]
115
+ SET existing_has_value.to = $at
116
+ }
117
+ WITH n, attr, existing_av
118
+ """
119
+ else:
120
+ update_value_query = """
121
+ // ------------
122
+ // Find the Nodes and Attributes we need to update
123
+ // ------------
124
+ MATCH (n:Node)
125
+ WHERE n.uuid IN $node_uuids
126
+ CALL (n) {
127
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
128
+ WHERE %(branch_filter)s
129
+ RETURN r.status = "active" AS is_active
130
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
131
+ LIMIT 1
132
+ }
133
+ WITH n, is_active
134
+ WHERE is_active = TRUE
135
+ WITH DISTINCT n
136
+ CALL (n) {
137
+ MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
138
+ WHERE %(branch_filter)s
139
+ RETURN attr, r.status = "active" AS is_active
140
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
141
+ LIMIT 1
142
+ }
143
+ WITH DISTINCT n, attr, is_active
144
+ WHERE is_active = TRUE
145
+ // ------------
146
+ // If the attribute has an existing value on the branch, then set the to time on it
147
+ // but only if the value is different from the new value
148
+ // ------------
149
+ CALL (n, attr) {
150
+ OPTIONAL MATCH (attr)-[r:HAS_VALUE]->(existing_av)
151
+ WHERE %(branch_filter)s
152
+ WITH r, existing_av, n
153
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
154
+ LIMIT 1
155
+ WITH CASE
156
+ WHEN existing_av.value <> $values_by_id[n.uuid]
157
+ AND r.status = "active"
158
+ AND r.branch = $branch
159
+ THEN [r, existing_av]
160
+ ELSE [NULL, NULL]
161
+ END AS existing_details
162
+ RETURN existing_details[0] AS existing_has_value, existing_details[1] AS existing_av
163
+ }
164
+ CALL (existing_has_value) {
165
+ WITH existing_has_value
166
+ WHERE existing_has_value IS NOT NULL
167
+ SET existing_has_value.to = $at
168
+ }
169
+ WITH n, attr, existing_av
170
+ """ % {"branch_filter": branch_filter}
171
+ self.add_to_query(update_value_query)
172
+
173
+ set_value_query = """
174
+ // ------------
175
+ // only make updates if the existing value is not the same as the new value
176
+ // ------------
177
+ WITH n, attr, existing_av, $values_by_id[n.uuid] AS required_value
178
+ WHERE existing_av.value <> required_value
179
+ OR existing_av IS NULL
180
+ CALL (n, attr) {
181
+ MERGE (av:AttributeValue&AttributeValueIndexed {is_default: false, value: $values_by_id[n.uuid]} )
182
+ WITH av, attr
183
+ LIMIT 1
184
+ CREATE (attr)-[r:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
185
+ }
186
+ """
187
+ self.add_to_query(set_value_query)
188
+
189
+
190
+ class GetNodesWithoutDisplayLabelQuery(Query):
191
+ """Get all active nodes that do not have a display_label attribute on the default branch."""
192
+
193
+ name = "get_nodes_without_display_label"
194
+ type = QueryType.READ
195
+
196
+ def __init__(self, kinds_to_skip: list[str] | None = None, **kwargs: Any) -> None:
197
+ super().__init__(**kwargs)
198
+ self.kinds_to_skip = kinds_to_skip or []
199
+
200
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
201
+ self.params = {
202
+ "branch_names": [registry.default_branch, GLOBAL_BRANCH_NAME],
203
+ "kinds_to_skip": self.kinds_to_skip,
204
+ "attribute_name": "display_label",
205
+ }
206
+ query = """
207
+ // ------------
208
+ // Get all active nodes that don't have a display_label attribute
209
+ // ------------
210
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
211
+ WHERE NOT n.kind IN $kinds_to_skip
212
+ AND e.branch IN $branch_names
213
+ AND e.status = "active"
214
+ AND e.to IS NULL
215
+ AND NOT exists((n)-[:IS_PART_OF {branch: e.branch, status: "deleted"}]->(:Root))
216
+ WITH DISTINCT n
217
+ CALL (n) {
218
+ OPTIONAL MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
219
+ WHERE r.branch IN $branch_names
220
+ RETURN r AS has_attr_e
221
+ ORDER BY r.from DESC, r.status ASC
222
+ LIMIT 1
223
+ }
224
+ WITH n, has_attr_e
225
+ WHERE (has_attr_e IS NULL OR has_attr_e.status = "deleted")
226
+ WITH n.uuid AS node_uuid
227
+ """
228
+ self.add_to_query(query)
229
+ self.return_labels = ["node_uuid"]
230
+
231
+ def get_node_uuids(self) -> list[str]:
232
+ return [result.get_as_type(label="node_uuid", return_type=str) for result in self.get_results()]
233
+
234
+
235
+ class GetNodesWithoutDisplayLabelBranchQuery(Query):
236
+ """Get all active nodes that do not have a display_label attribute on a non-default branch."""
237
+
238
+ name = "get_nodes_without_display_label_branch"
239
+ type = QueryType.READ
240
+
241
+ def __init__(self, kinds_to_skip: list[str] | None = None, **kwargs: Any) -> None:
242
+ super().__init__(**kwargs)
243
+ self.kinds_to_skip = kinds_to_skip or []
244
+
245
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
246
+ branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
247
+ self.params = {
248
+ "kinds_to_skip": self.kinds_to_skip,
249
+ "attribute_name": "display_label",
250
+ **branch_filter_params,
251
+ }
252
+ query = """
253
+ // ------------
254
+ // Get all active nodes that don't have a display_label attribute
255
+ // ------------
256
+ MATCH (n:Node)
257
+ WHERE NOT n.kind IN $kinds_to_skip
258
+ CALL (n) {
259
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
260
+ WHERE %(branch_filter)s
261
+ RETURN r AS is_part_of_e
262
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
263
+ LIMIT 1
264
+ }
265
+ WITH n, is_part_of_e
266
+ WHERE is_part_of_e.status = "active"
267
+ CALL (n) {
268
+ OPTIONAL MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
269
+ WHERE %(branch_filter)s
270
+ RETURN r AS has_attr_e
271
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
272
+ LIMIT 1
273
+ }
274
+ WITH n, has_attr_e
275
+ WHERE (has_attr_e IS NULL OR has_attr_e.status = "deleted")
276
+ WITH n.uuid AS node_uuid
277
+ """ % {"branch_filter": branch_filter}
278
+ self.add_to_query(query)
279
+ self.return_labels = ["node_uuid"]
280
+
281
+ def get_node_uuids(self) -> list[str]:
282
+ return [result.get_as_type(label="node_uuid", return_type=str) for result in self.get_results()]
283
+
284
+
285
+ class CreateDisplayLabelNullQuery(Query):
286
+ """Create display_label attribute with NULL value for the given nodes."""
287
+
288
+ name = "create_display_label_null"
289
+ type = QueryType.WRITE
290
+ insert_return = False
291
+
292
+ def __init__(self, node_uuids: list[str], **kwargs: Any) -> None:
293
+ super().__init__(**kwargs)
294
+ self.node_uuids = node_uuids
295
+
296
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
297
+ self.params = {
298
+ "node_uuids": self.node_uuids,
299
+ "attribute_name": "display_label",
300
+ "default_branch": registry.default_branch,
301
+ "global_branch": GLOBAL_BRANCH_NAME,
302
+ "branch": self.branch.name,
303
+ "branch_level": self.branch.hierarchy_level,
304
+ "at": self.at.to_string(),
305
+ "null_value": NULL_VALUE,
306
+ "branch_support": "aware",
307
+ "is_protected_default": False,
308
+ "is_visible_default": True,
309
+ }
310
+ branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
311
+ self.params.update(branch_filter_params)
312
+
313
+ # Create the NULL AttributeValue first
314
+ create_av_query = """
315
+ MERGE (av:AttributeValue&AttributeValueIndexed {is_default: true, value: $null_value})
316
+ WITH av
317
+ LIMIT 1
318
+ MERGE (is_protected_value:Boolean { value: $is_protected_default })
319
+ MERGE (is_visible_value:Boolean { value: $is_visible_default })
320
+ """
321
+ self.add_to_query(create_av_query)
322
+
323
+ if self.branch.name in [registry.default_branch, GLOBAL_BRANCH_NAME]:
324
+ query = """
325
+ // ------------
326
+ // Create the display_label attribute with NULL value for nodes
327
+ // ------------
328
+ WITH av, is_protected_value, is_visible_value
329
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
330
+ WHERE n.uuid IN $node_uuids
331
+ AND e.branch IN [$default_branch, $global_branch]
332
+ AND e.to IS NULL
333
+ AND e.status = "active"
334
+ CREATE (a:Attribute { uuid: randomUUID(), name: $attribute_name, branch_support: $branch_support })
335
+ CREATE (n)-[:HAS_ATTRIBUTE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(a)
336
+ CREATE (a)-[:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
337
+ CREATE (a)-[:IS_PROTECTED { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_protected_value)
338
+ CREATE (a)-[:IS_VISIBLE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_visible_value)
339
+ """
340
+ else:
341
+ query = """
342
+ // ------------
343
+ // Create the display_label attribute with NULL value for nodes
344
+ // ------------
345
+ WITH av, is_protected_value, is_visible_value
346
+ MATCH (n:Node)
347
+ WHERE n.uuid IN $node_uuids
348
+ CALL (n) {
349
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
350
+ WHERE %(branch_filter)s
351
+ RETURN r.status = "active" AS is_active
352
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
353
+ LIMIT 1
354
+ }
355
+ WITH n, is_active, av, is_protected_value, is_visible_value
356
+ WHERE is_active = TRUE
357
+ CREATE (a:Attribute { uuid: randomUUID(), name: $attribute_name, branch_support: $branch_support })
358
+ CREATE (n)-[:HAS_ATTRIBUTE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(a)
359
+ CREATE (a)-[:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
360
+ CREATE (a)-[:IS_PROTECTED { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_protected_value)
361
+ CREATE (a)-[:IS_VISIBLE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_visible_value)
362
+ """ % {"branch_filter": branch_filter}
363
+
364
+ self.add_to_query(query)
365
+
366
+
367
+ class Migration047(MigrationRequiringRebase):
368
+ """
369
+ Backfill `display_label` attributes for all nodes:
370
+ - If schema does not define display_label OR attribute doesn't exist: insert NULL value
371
+ - If schema defines display_label: compute and store the value, invalidate NULL value if exists
372
+ """
373
+
374
+ name: str = "047_backfill_or_null_display_label"
375
+ minimum_version: int = 46
376
+ update_batch_size: int = 1000
377
+ # skip these b/c the attributes on these schema-related nodes are used to define the values included in
378
+ # the display_label attributes on instances of these schema, so should not be updated
379
+ kinds_to_skip: list[str] = ["SchemaNode", "SchemaAttribute", "SchemaRelationship", "SchemaGeneric"]
380
+
381
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
382
+ return MigrationResult()
383
+
384
+ def _extract_schema_paths_from_display_label(
385
+ self, schema: MainSchemaTypes, schema_branch: SchemaBranch
386
+ ) -> list[SchemaAttributePath]:
387
+ """Extract schema paths from display_label, handling both simple paths and Jinja2 templates.
388
+
389
+ This follows the same logic as _validate_display_label in schema_branch.py.
390
+ """
391
+ if not schema.display_label:
392
+ return []
393
+
394
+ if not _is_jinja2_template(schema.display_label):
395
+ schema_path = schema.parse_schema_path(path=schema.display_label, schema=schema_branch)
396
+ return [schema_path]
397
+
398
+ schema_paths = []
399
+ for variable in _extract_jinja2_variables(schema.display_label):
400
+ schema_path = schema.parse_schema_path(path=variable, schema=schema_branch)
401
+ schema_paths.append(schema_path)
402
+
403
+ return schema_paths
404
+
405
+ async def _do_one_schema_all(
406
+ self,
407
+ db: InfrahubDatabase,
408
+ branch: Branch,
409
+ schema: MainSchemaTypes,
410
+ schema_branch: SchemaBranch,
411
+ attribute_schema: AttributeSchema,
412
+ progress: Progress | None = None,
413
+ update_task: TaskID | None = None,
414
+ ) -> None:
415
+ if not schema.display_label:
416
+ return
417
+
418
+ schema_paths = self._extract_schema_paths_from_display_label(schema=schema, schema_branch=schema_branch)
419
+ if not schema_paths:
420
+ return
421
+
422
+ offset = 0
423
+
424
+ # loop until we get no results from the get_details_query
425
+ while True:
426
+ if branch.is_default:
427
+ get_details_query: GetResultMapQuery = await GetPathDetailsDefaultBranch.init(
428
+ db=db,
429
+ schema_kind=schema.kind,
430
+ schema_paths=schema_paths,
431
+ offset=offset,
432
+ limit=self.update_batch_size,
433
+ )
434
+ else:
435
+ get_details_query = await GetPathDetailsBranchQuery.init(
436
+ db=db,
437
+ branch=branch,
438
+ schema_kind=schema.kind,
439
+ schema_paths=schema_paths,
440
+ updates_only=False,
441
+ offset=offset,
442
+ limit=self.update_batch_size,
443
+ )
444
+ await get_details_query.execute(db=db)
445
+
446
+ # Get the values for all schema paths
447
+ schema_path_values_map = get_details_query.get_result_map(schema_paths)
448
+ num_updates = len(schema_path_values_map)
449
+
450
+ formatted_schema_path_values_map: dict[str, str] = {}
451
+ for k, v in schema_path_values_map.items():
452
+ if not v:
453
+ continue
454
+
455
+ rendered_value = await _render_display_label(
456
+ display_label=schema.display_label,
457
+ variable_names=[s.attribute_path_as_str for s in schema_paths],
458
+ values=v,
459
+ )
460
+ if rendered_value is not None:
461
+ formatted_schema_path_values_map[k] = rendered_value
462
+
463
+ if formatted_schema_path_values_map:
464
+ update_display_label_query = await UpdateAttributeValuesQuery.init(
465
+ db=db,
466
+ branch=branch,
467
+ attribute_schema=attribute_schema,
468
+ values_by_id_map=formatted_schema_path_values_map,
469
+ )
470
+ await update_display_label_query.execute(db=db)
471
+
472
+ if progress is not None and update_task is not None:
473
+ progress.update(update_task, advance=num_updates)
474
+
475
+ if num_updates == 0:
476
+ break
477
+
478
+ offset += self.update_batch_size
479
+
480
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
481
+ root_node = await get_root_node(db=db, initialize=False)
482
+ default_branch_name = root_node.default_branch
483
+ default_branch = await Branch.get_by_name(db=db, name=default_branch_name)
484
+
485
+ main_schema_branch = await get_or_load_schema_branch(db=db, branch=default_branch)
486
+
487
+ base_node_schema = main_schema_branch.get("SchemaNode", duplicate=False)
488
+ display_label_attribute_schema = base_node_schema.get_attribute("display_label")
489
+
490
+ # Get nodes without display_label in the database
491
+ get_nodes_without_dl_query = await GetNodesWithoutDisplayLabelQuery.init(
492
+ db=db, kinds_to_skip=self.kinds_to_skip
493
+ )
494
+ await get_nodes_without_dl_query.execute(db=db)
495
+ nodes_without_display_label = get_nodes_without_dl_query.get_node_uuids()
496
+
497
+ # Count nodes that will get computed values
498
+ kinds_to_backfill: list[str] = []
499
+ for node_schema_name in (
500
+ main_schema_branch.node_names + main_schema_branch.profile_names + main_schema_branch.template_names
501
+ ):
502
+ if node_schema_name in self.kinds_to_skip:
503
+ continue
504
+
505
+ node_schema = main_schema_branch.get(name=node_schema_name, duplicate=False)
506
+ if node_schema.branch != BranchSupportType.AWARE or not node_schema.display_label:
507
+ continue
508
+
509
+ kinds_to_backfill.append(node_schema.kind)
510
+
511
+ backfill_count = 0
512
+ if kinds_to_backfill:
513
+ count_query = await DefaultBranchNodeCount.init(
514
+ db=db, kinds_to_skip=self.kinds_to_skip, kinds_to_include=kinds_to_backfill
515
+ )
516
+ await count_query.execute(db=db)
517
+ backfill_count = count_query.get_num_nodes()
518
+
519
+ try:
520
+ with Progress(console=console) as progress:
521
+ # Create NULL display_label
522
+ if nodes_without_display_label:
523
+ null_task = progress.add_task(
524
+ f"Creating NULL display_label for {len(nodes_without_display_label)} nodes",
525
+ total=len(nodes_without_display_label),
526
+ )
527
+
528
+ for offset in range(0, len(nodes_without_display_label), self.update_batch_size):
529
+ batch_uuids = nodes_without_display_label[offset : offset + self.update_batch_size]
530
+ if not batch_uuids:
531
+ break
532
+
533
+ create_display_label_query = await CreateDisplayLabelNullQuery.init(
534
+ db=db, branch=default_branch, node_uuids=batch_uuids
535
+ )
536
+ await create_display_label_query.execute(db=db)
537
+
538
+ progress.update(null_task, advance=len(batch_uuids))
539
+
540
+ # Backfill computed display_label values
541
+ if backfill_count > 0:
542
+ backfill_task = progress.add_task(
543
+ f"Backfilling computed display_label for {backfill_count} nodes",
544
+ total=backfill_count,
545
+ )
546
+
547
+ for node_schema_name in kinds_to_backfill:
548
+ await self._do_one_schema_all(
549
+ db=db,
550
+ branch=default_branch,
551
+ schema=main_schema_branch.get(name=node_schema_name, duplicate=False),
552
+ schema_branch=main_schema_branch,
553
+ attribute_schema=display_label_attribute_schema,
554
+ progress=progress,
555
+ update_task=backfill_task,
556
+ )
557
+
558
+ except Exception as exc:
559
+ return MigrationResult(errors=[str(exc)])
560
+ return MigrationResult()
561
+
562
+ async def execute_against_branch(self, db: InfrahubDatabase, branch: Branch) -> MigrationResult:
563
+ schema_branch = await get_or_load_schema_branch(db=db, branch=branch)
564
+
565
+ base_node_schema = schema_branch.get("SchemaNode", duplicate=False)
566
+ display_label_attribute_schema = base_node_schema.get_attribute("display_label")
567
+
568
+ try:
569
+ get_nodes_without_dl_query = await GetNodesWithoutDisplayLabelBranchQuery.init(
570
+ db=db, branch=branch, kinds_to_skip=self.kinds_to_skip
571
+ )
572
+ await get_nodes_without_dl_query.execute(db=db)
573
+ nodes_without_display_label = get_nodes_without_dl_query.get_node_uuids()
574
+
575
+ if nodes_without_display_label:
576
+ for offset in range(0, len(nodes_without_display_label), self.update_batch_size):
577
+ batch_uuids = nodes_without_display_label[offset : offset + self.update_batch_size]
578
+ if not batch_uuids:
579
+ break
580
+
581
+ create_display_label_query = await CreateDisplayLabelNullQuery.init(
582
+ db=db, branch=branch, node_uuids=batch_uuids
583
+ )
584
+ await create_display_label_query.execute(db=db)
585
+
586
+ for node_schema_name in (
587
+ schema_branch.node_names + schema_branch.profile_names + schema_branch.template_names
588
+ ):
589
+ if node_schema_name in self.kinds_to_skip:
590
+ continue
591
+
592
+ node_schema = schema_branch.get(name=node_schema_name, duplicate=False)
593
+ if node_schema.branch != BranchSupportType.AWARE or not node_schema.display_label:
594
+ continue
595
+
596
+ await self._do_one_schema_all(
597
+ db=db,
598
+ branch=branch,
599
+ schema=node_schema,
600
+ schema_branch=schema_branch,
601
+ attribute_schema=display_label_attribute_schema,
602
+ )
603
+
604
+ except Exception as exc:
605
+ return MigrationResult(errors=[str(exc)])
606
+ return MigrationResult()
infrahub/core/models.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import hashlib
4
5
  from typing import TYPE_CHECKING, Any
5
6
 
@@ -359,7 +360,7 @@ class SchemaUpdateValidationResult(BaseModel):
359
360
 
360
361
  def validate_migrations(self, migration_map: dict[str, Any]) -> None:
361
362
  for migration in self.migrations:
362
- if migration_map.get(migration.migration_name, None) is None:
363
+ if migration_map.get(migration.migration_name) is None:
363
364
  self.errors.append(
364
365
  SchemaUpdateValidationError(
365
366
  path=migration.path,
@@ -370,7 +371,7 @@ class SchemaUpdateValidationResult(BaseModel):
370
371
 
371
372
  def validate_constraints(self, validator_map: dict[str, Any]) -> None:
372
373
  for constraint in self.constraints:
373
- if validator_map.get(constraint.constraint_name, None) is None:
374
+ if validator_map.get(constraint.constraint_name) is None:
374
375
  self.errors.append(
375
376
  SchemaUpdateValidationError(
376
377
  path=constraint.path,
@@ -578,11 +579,9 @@ class HashableModel(BaseModel):
578
579
 
579
580
  for field_name in other.model_fields.keys():
580
581
  if not hasattr(self, field_name):
581
- try:
582
- setattr(self, field_name, getattr(other, field_name))
583
- except ValueError:
582
+ with contextlib.suppress(ValueError):
584
583
  # handles the case where self and other are different types and other has fields that self does not
585
- pass
584
+ setattr(self, field_name, getattr(other, field_name))
586
585
  continue
587
586
 
588
587
  attr_other = getattr(other, field_name)