infrahub-server 1.7.0rc0__py3-none-any.whl → 1.7.2__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 (124) hide show
  1. infrahub/actions/gather.py +2 -2
  2. infrahub/api/query.py +3 -2
  3. infrahub/api/schema.py +5 -0
  4. infrahub/api/transformation.py +3 -3
  5. infrahub/cli/db.py +6 -2
  6. infrahub/computed_attribute/gather.py +2 -0
  7. infrahub/config.py +2 -2
  8. infrahub/core/attribute.py +21 -2
  9. infrahub/core/branch/models.py +11 -117
  10. infrahub/core/branch/tasks.py +7 -3
  11. infrahub/core/diff/merger/merger.py +5 -1
  12. infrahub/core/diff/model/path.py +43 -0
  13. infrahub/core/graph/__init__.py +1 -1
  14. infrahub/core/graph/index.py +2 -0
  15. infrahub/core/initialization.py +2 -1
  16. infrahub/core/ipam/resource_allocator.py +229 -0
  17. infrahub/core/migrations/graph/__init__.py +10 -0
  18. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
  19. infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
  20. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
  21. infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
  22. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
  23. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
  24. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
  25. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
  26. infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
  27. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
  28. infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
  29. infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
  30. infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
  31. infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
  32. infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
  33. infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
  34. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
  35. infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
  36. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +3 -2
  37. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
  38. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
  39. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
  40. infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
  41. infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
  42. infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
  43. infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
  44. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +3 -3
  45. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
  46. infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
  47. infrahub/core/migrations/graph/m052_fix_global_branch_level.py +51 -0
  48. infrahub/core/migrations/graph/m053_fix_branch_level_zero.py +61 -0
  49. infrahub/core/migrations/graph/m054_cleanup_orphaned_nodes.py +87 -0
  50. infrahub/core/migrations/graph/m055_remove_webhook_validate_certificates_default.py +86 -0
  51. infrahub/core/migrations/runner.py +6 -3
  52. infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
  53. infrahub/core/migrations/schema/attribute_supports_profile.py +3 -8
  54. infrahub/core/migrations/schema/models.py +8 -0
  55. infrahub/core/migrations/schema/node_attribute_add.py +24 -29
  56. infrahub/core/migrations/schema/tasks.py +7 -1
  57. infrahub/core/migrations/shared.py +37 -30
  58. infrahub/core/node/__init__.py +2 -1
  59. infrahub/core/node/lock_utils.py +23 -2
  60. infrahub/core/node/resource_manager/ip_address_pool.py +5 -11
  61. infrahub/core/node/resource_manager/ip_prefix_pool.py +5 -21
  62. infrahub/core/node/resource_manager/number_pool.py +109 -39
  63. infrahub/core/query/__init__.py +7 -1
  64. infrahub/core/query/branch.py +18 -2
  65. infrahub/core/query/ipam.py +629 -40
  66. infrahub/core/query/node.py +128 -0
  67. infrahub/core/query/resource_manager.py +114 -1
  68. infrahub/core/relationship/model.py +9 -3
  69. infrahub/core/schema/attribute_parameters.py +28 -1
  70. infrahub/core/schema/attribute_schema.py +9 -2
  71. infrahub/core/schema/definitions/core/webhook.py +0 -1
  72. infrahub/core/schema/definitions/internal.py +7 -4
  73. infrahub/core/schema/manager.py +50 -38
  74. infrahub/core/validators/attribute/kind.py +5 -2
  75. infrahub/core/validators/determiner.py +4 -0
  76. infrahub/graphql/analyzer.py +3 -1
  77. infrahub/graphql/app.py +7 -10
  78. infrahub/graphql/execution.py +95 -0
  79. infrahub/graphql/manager.py +8 -2
  80. infrahub/graphql/mutations/proposed_change.py +15 -0
  81. infrahub/graphql/parser.py +10 -7
  82. infrahub/graphql/queries/ipam.py +20 -25
  83. infrahub/graphql/queries/search.py +29 -9
  84. infrahub/lock.py +7 -0
  85. infrahub/proposed_change/tasks.py +2 -0
  86. infrahub/services/adapters/cache/redis.py +7 -0
  87. infrahub/services/adapters/http/httpx.py +27 -0
  88. infrahub/trigger/catalogue.py +2 -0
  89. infrahub/trigger/models.py +73 -4
  90. infrahub/trigger/setup.py +1 -1
  91. infrahub/trigger/system.py +36 -0
  92. infrahub/webhook/models.py +4 -2
  93. infrahub/webhook/tasks.py +2 -2
  94. infrahub/workflows/initialization.py +2 -2
  95. infrahub_sdk/analyzer.py +2 -2
  96. infrahub_sdk/branch.py +12 -39
  97. infrahub_sdk/checks.py +4 -4
  98. infrahub_sdk/client.py +36 -0
  99. infrahub_sdk/ctl/cli_commands.py +2 -1
  100. infrahub_sdk/ctl/graphql.py +15 -4
  101. infrahub_sdk/ctl/utils.py +2 -2
  102. infrahub_sdk/enums.py +6 -0
  103. infrahub_sdk/graphql/renderers.py +21 -0
  104. infrahub_sdk/graphql/utils.py +85 -0
  105. infrahub_sdk/node/attribute.py +12 -2
  106. infrahub_sdk/node/constants.py +11 -0
  107. infrahub_sdk/node/metadata.py +69 -0
  108. infrahub_sdk/node/node.py +65 -14
  109. infrahub_sdk/node/property.py +3 -0
  110. infrahub_sdk/node/related_node.py +24 -1
  111. infrahub_sdk/node/relationship.py +10 -1
  112. infrahub_sdk/operation.py +2 -2
  113. infrahub_sdk/schema/repository.py +1 -2
  114. infrahub_sdk/transforms.py +2 -2
  115. infrahub_sdk/types.py +18 -2
  116. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/METADATA +8 -8
  117. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/RECORD +123 -114
  118. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/entry_points.txt +0 -1
  119. infrahub_testcontainers/docker-compose-cluster.test.yml +16 -10
  120. infrahub_testcontainers/docker-compose.test.yml +11 -10
  121. infrahub_testcontainers/performance_test.py +1 -1
  122. infrahub/pools/address.py +0 -16
  123. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/WHEEL +0 -0
  124. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/licenses/LICENSE.txt +0 -0
@@ -25,6 +25,7 @@ from infrahub.core.schema import (
25
25
  SchemaRoot,
26
26
  TemplateSchema,
27
27
  )
28
+ from infrahub.core.timestamp import Timestamp
28
29
  from infrahub.core.utils import parse_node_kind
29
30
  from infrahub.exceptions import SchemaNotFoundError
30
31
  from infrahub.log import get_logger
@@ -36,7 +37,6 @@ log = get_logger()
36
37
 
37
38
  if TYPE_CHECKING:
38
39
  from infrahub.core.branch import Branch
39
- from infrahub.core.timestamp import Timestamp
40
40
  from infrahub.database import InfrahubDatabase
41
41
 
42
42
 
@@ -179,18 +179,20 @@ class SchemaManager(NodeManager):
179
179
  diff: SchemaDiff | None = None,
180
180
  limit: list[str] | None = None,
181
181
  update_db: bool = True,
182
+ at: Timestamp | None = None,
182
183
  user_id: str = SYSTEM_USER_ID,
183
184
  ) -> None:
184
185
  branch = await registry.get_branch(branch=branch, db=db)
186
+ at = Timestamp(at)
185
187
 
186
188
  updated_schema = None
187
189
  if update_db:
188
190
  if diff:
189
191
  schema_diff = await self.update_schema_to_db(
190
- schema=schema, db=db, branch=branch, diff=diff, user_id=user_id
192
+ schema=schema, db=db, branch=branch, diff=diff, at=at, user_id=user_id
191
193
  )
192
194
  else:
193
- await self.load_schema_to_db(schema=schema, db=db, branch=branch, limit=limit, user_id=user_id)
195
+ await self.load_schema_to_db(schema=schema, db=db, branch=branch, limit=limit, at=at, user_id=user_id)
194
196
  # After updating the schema into the db
195
197
  # we need to pull a fresh version because some default value are managed/generated within the node object
196
198
  schema_diff = None
@@ -201,7 +203,7 @@ class SchemaManager(NodeManager):
201
203
  )
202
204
 
203
205
  updated_schema = await self.load_schema_from_db(
204
- db=db, branch=branch, schema=schema, schema_diff=schema_diff
206
+ db=db, branch=branch, schema=schema, schema_diff=schema_diff, at=at
205
207
  )
206
208
 
207
209
  self.set_schema_branch(name=branch.name, schema=updated_schema or schema)
@@ -221,6 +223,7 @@ class SchemaManager(NodeManager):
221
223
  db: InfrahubDatabase,
222
224
  diff: SchemaDiff,
223
225
  user_id: str,
226
+ at: Timestamp,
224
227
  branch: Branch | str | None = None,
225
228
  ) -> SchemaBranchDiff:
226
229
  """Load all nodes, generics and groups from a SchemaRoot object into the database."""
@@ -231,7 +234,7 @@ class SchemaManager(NodeManager):
231
234
  added_generics = []
232
235
  for item_kind in diff.added.keys():
233
236
  item = schema.get(name=item_kind, duplicate=False)
234
- node = await self.load_node_to_db(node=item, branch=branch, db=db, user_id=user_id)
237
+ node = await self.load_node_to_db(node=item, branch=branch, db=db, at=at, user_id=user_id)
235
238
  schema.set(name=item_kind, schema=node)
236
239
  if item.is_node_schema:
237
240
  added_nodes.append(item_kind)
@@ -244,10 +247,10 @@ class SchemaManager(NodeManager):
244
247
  item = schema.get(name=item_kind, duplicate=False)
245
248
  if item_diff:
246
249
  node = await self.update_node_in_db_based_on_diff(
247
- node=item, branch=branch, db=db, diff=item_diff, user_id=user_id
250
+ node=item, branch=branch, db=db, diff=item_diff, at=at, user_id=user_id
248
251
  )
249
252
  else:
250
- node = await self.update_node_in_db(node=item, branch=branch, db=db, user_id=user_id)
253
+ node = await self.update_node_in_db(node=item, branch=branch, db=db, at=at, user_id=user_id)
251
254
  schema.set(name=item_kind, schema=node)
252
255
  if item.is_node_schema:
253
256
  changed_nodes.append(item_kind)
@@ -258,7 +261,7 @@ class SchemaManager(NodeManager):
258
261
  removed_generics = []
259
262
  for item_kind in diff.removed.keys():
260
263
  item = schema.get(name=item_kind, duplicate=False)
261
- node = await self.delete_node_in_db(node=item, branch=branch, db=db, user_id=user_id)
264
+ node = await self.delete_node_in_db(node=item, branch=branch, db=db, at=at, user_id=user_id)
262
265
  schema.delete(name=item_kind)
263
266
  if item.is_node_schema:
264
267
  removed_nodes.append(item_kind)
@@ -281,9 +284,10 @@ class SchemaManager(NodeManager):
281
284
  branch: Branch | str | None = None,
282
285
  limit: list[str] | None = None,
283
286
  user_id: str = SYSTEM_USER_ID,
287
+ at: Timestamp | None = None,
284
288
  ) -> None:
285
289
  """Load all nodes, generics and groups from a SchemaRoot object into the database."""
286
-
290
+ at = Timestamp(at)
287
291
  branch = await registry.get_branch(branch=branch, db=db)
288
292
 
289
293
  for item_kind in schema.node_names + schema.generic_names_without_templates:
@@ -291,10 +295,10 @@ class SchemaManager(NodeManager):
291
295
  continue
292
296
  item = schema.get(name=item_kind, duplicate=False)
293
297
  if not item.id:
294
- node = await self.load_node_to_db(node=item, branch=branch, db=db, user_id=user_id)
298
+ node = await self.load_node_to_db(node=item, branch=branch, db=db, at=at, user_id=user_id)
295
299
  schema.set(name=item_kind, schema=node)
296
300
  else:
297
- node = await self.update_node_in_db(node=item, branch=branch, db=db, user_id=user_id)
301
+ node = await self.update_node_in_db(node=item, branch=branch, db=db, at=at, user_id=user_id)
298
302
  schema.set(name=item_kind, schema=node)
299
303
 
300
304
  async def load_node_to_db(
@@ -302,6 +306,7 @@ class SchemaManager(NodeManager):
302
306
  node: NodeSchema | GenericSchema,
303
307
  db: InfrahubDatabase,
304
308
  user_id: str,
309
+ at: Timestamp,
305
310
  branch: Branch | str | None = None,
306
311
  ) -> NodeSchema | GenericSchema:
307
312
  """Load a Node with its attributes and its relationships to the database."""
@@ -322,7 +327,7 @@ class SchemaManager(NodeManager):
322
327
  schema_dict = node.model_dump(exclude={"id", "state", "filters", "relationships", "attributes"})
323
328
  obj = await Node.init(schema=node_schema, branch=branch, db=db)
324
329
  await obj.new(**schema_dict, db=db)
325
- await obj.save(db=db, user_id=user_id)
330
+ await obj.save(db=db, at=at, user_id=user_id)
326
331
  new_node.id = obj.id
327
332
 
328
333
  # Then create the Attributes and the relationships
@@ -333,7 +338,7 @@ class SchemaManager(NodeManager):
333
338
  for item in node.attributes:
334
339
  if item.inherited is False:
335
340
  new_attr = await self.create_attribute_in_db(
336
- schema=attribute_schema, item=item, parent=obj, branch=branch, db=db, user_id=user_id
341
+ schema=attribute_schema, item=item, parent=obj, branch=branch, db=db, at=at, user_id=user_id
337
342
  )
338
343
  else:
339
344
  new_attr = item.duplicate()
@@ -342,7 +347,7 @@ class SchemaManager(NodeManager):
342
347
  for item in node.relationships:
343
348
  if item.inherited is False:
344
349
  new_rel = await self.create_relationship_in_db(
345
- schema=relationship_schema, item=item, parent=obj, branch=branch, db=db, user_id=user_id
350
+ schema=relationship_schema, item=item, parent=obj, branch=branch, db=db, at=at, user_id=user_id
346
351
  )
347
352
  else:
348
353
  new_rel = item.duplicate()
@@ -357,6 +362,7 @@ class SchemaManager(NodeManager):
357
362
  db: InfrahubDatabase,
358
363
  node: NodeSchema | GenericSchema,
359
364
  user_id: str,
365
+ at: Timestamp,
360
366
  branch: Branch | str | None = None,
361
367
  ) -> NodeSchema | GenericSchema:
362
368
  """Update a Node with its attributes and its relationships in the database."""
@@ -380,11 +386,11 @@ class SchemaManager(NodeManager):
380
386
  new_node = node.duplicate()
381
387
 
382
388
  # Update the attributes and the relationships nodes as well
383
- await obj.attributes.update(db=db, data=[item.id for item in node.local_attributes if item.id])
389
+ await obj.attributes.update(db=db, data=[item.id for item in node.local_attributes if item.id], at=at)
384
390
  await obj.relationships.update(
385
- db=db, data=[item.id for item in node.local_relationships if item.id and item.name != "profiles"]
391
+ db=db, data=[item.id for item in node.local_relationships if item.id and item.name != "profiles"], at=at
386
392
  )
387
- await obj.save(db=db, user_id=user_id)
393
+ await obj.save(db=db, at=at, user_id=user_id)
388
394
 
389
395
  # Then Update the Attributes and the relationships
390
396
 
@@ -397,19 +403,19 @@ class SchemaManager(NodeManager):
397
403
 
398
404
  for item in node.local_attributes:
399
405
  if item.id and item.id in items:
400
- await self.update_attribute_in_db(item=item, attr=items[item.id], db=db, user_id=user_id)
406
+ await self.update_attribute_in_db(item=item, attr=items[item.id], db=db, at=at, user_id=user_id)
401
407
  elif not item.id:
402
408
  new_attr = await self.create_attribute_in_db(
403
- schema=attribute_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
409
+ schema=attribute_schema, item=item, branch=branch, db=db, parent=obj, at=at, user_id=user_id
404
410
  )
405
411
  new_node.attributes.append(new_attr)
406
412
 
407
413
  for item in node.local_relationships:
408
414
  if item.id and item.id in items:
409
- await self.update_relationship_in_db(item=item, rel=items[item.id], db=db, user_id=user_id)
415
+ await self.update_relationship_in_db(item=item, rel=items[item.id], db=db, at=at, user_id=user_id)
410
416
  elif not item.id:
411
417
  new_rel = await self.create_relationship_in_db(
412
- schema=relationship_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
418
+ schema=relationship_schema, item=item, branch=branch, db=db, parent=obj, at=at, user_id=user_id
413
419
  )
414
420
  new_node.relationships.append(new_rel)
415
421
 
@@ -423,6 +429,7 @@ class SchemaManager(NodeManager):
423
429
  diff: HashableModelDiff,
424
430
  node: NodeSchema | GenericSchema,
425
431
  user_id: str,
432
+ at: Timestamp,
426
433
  branch: Branch | str | None = None,
427
434
  ) -> NodeSchema | GenericSchema:
428
435
  """Update a Node with its attributes and its relationships in the database based on a HashableModelDiff."""
@@ -496,24 +503,24 @@ class SchemaManager(NodeManager):
496
503
  items.update({field.id: field for field in missing_attrs + missing_rels})
497
504
 
498
505
  if diff_attributes:
499
- await obj.attributes.update(db=db, data=[item.id for item in node.local_attributes if item.id])
506
+ await obj.attributes.update(db=db, data=[item.id for item in node.local_attributes if item.id], at=at)
500
507
 
501
508
  if diff_relationships:
502
- await obj.relationships.update(db=db, data=[item.id for item in node.local_relationships if item.id])
509
+ await obj.relationships.update(db=db, data=[item.id for item in node.local_relationships if item.id], at=at)
503
510
 
504
- await obj.save(db=db, user_id=user_id)
511
+ await obj.save(db=db, at=at, user_id=user_id)
505
512
 
506
513
  if diff_attributes:
507
514
  for item in node.local_attributes:
508
515
  # if item is in changed and has no ID, then it is being overridden from a generic and must be added
509
516
  if item.name in diff_attributes.added or (item.name in diff_attributes.changed and item.id is None):
510
517
  created_item = await self.create_attribute_in_db(
511
- schema=attribute_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
518
+ schema=attribute_schema, item=item, branch=branch, db=db, parent=obj, at=at, user_id=user_id
512
519
  )
513
520
  new_attr = new_node.get_attribute(name=item.name)
514
521
  new_attr.id = created_item.id
515
522
  elif item.name in diff_attributes.changed and item.id and item.id in items:
516
- await self.update_attribute_in_db(item=item, attr=items[item.id], db=db, user_id=user_id)
523
+ await self.update_attribute_in_db(item=item, attr=items[item.id], db=db, at=at, user_id=user_id)
517
524
  elif item.name in diff_attributes.removed and item.id and item.id in items:
518
525
  await items[item.id].delete(db=db, user_id=user_id)
519
526
  elif (
@@ -530,12 +537,12 @@ class SchemaManager(NodeManager):
530
537
  item.name in diff_relationships.changed and item.id is None
531
538
  ):
532
539
  created_rel = await self.create_relationship_in_db(
533
- schema=relationship_schema, item=item, branch=branch, db=db, parent=obj, user_id=user_id
540
+ schema=relationship_schema, item=item, branch=branch, db=db, parent=obj, at=at, user_id=user_id
534
541
  )
535
542
  new_rel = new_node.get_relationship(name=item.name)
536
543
  new_rel.id = created_rel.id
537
544
  elif item.name in diff_relationships.changed and item.id and item.id in items:
538
- await self.update_relationship_in_db(item=item, rel=items[item.id], db=db, user_id=user_id)
545
+ await self.update_relationship_in_db(item=item, rel=items[item.id], db=db, at=at, user_id=user_id)
539
546
  elif item.name in diff_relationships.removed and item.id and item.id in items:
540
547
  await items[item.id].delete(db=db, user_id=user_id)
541
548
  elif (
@@ -555,7 +562,7 @@ class SchemaManager(NodeManager):
555
562
  if field_names_to_remove:
556
563
  for field_schema in items.values():
557
564
  if field_schema.name.value in field_names_to_remove:
558
- await field_schema.delete(db=db, user_id=user_id)
565
+ await field_schema.delete(db=db, at=at, user_id=user_id)
559
566
 
560
567
  # Save back the node with the (potentially) newly created IDs in the SchemaManager
561
568
  self.set(name=new_node.kind, schema=new_node, branch=branch.name)
@@ -566,6 +573,7 @@ class SchemaManager(NodeManager):
566
573
  db: InfrahubDatabase,
567
574
  node: NodeSchema | GenericSchema,
568
575
  user_id: str,
576
+ at: Timestamp,
569
577
  branch: Branch | str | None = None,
570
578
  ) -> None:
571
579
  """Delete the node with its attributes and relationships."""
@@ -581,11 +589,11 @@ class SchemaManager(NodeManager):
581
589
 
582
590
  # First delete the attributes and the relationships
583
591
  for attr_schema_node in (await obj.attributes.get_peers(db=db)).values():
584
- await attr_schema_node.delete(db=db, user_id=user_id)
592
+ await attr_schema_node.delete(db=db, at=at, user_id=user_id)
585
593
  for rel_schema_node in (await obj.relationships.get_peers(db=db)).values():
586
- await rel_schema_node.delete(db=db, user_id=user_id)
594
+ await rel_schema_node.delete(db=db, at=at, user_id=user_id)
587
595
 
588
- await obj.delete(db=db, user_id=user_id)
596
+ await obj.delete(db=db, at=at, user_id=user_id)
589
597
 
590
598
  @staticmethod
591
599
  async def create_attribute_in_db(
@@ -595,20 +603,23 @@ class SchemaManager(NodeManager):
595
603
  parent: Node,
596
604
  db: InfrahubDatabase,
597
605
  user_id: str,
606
+ at: Timestamp,
598
607
  ) -> AttributeSchema:
599
608
  obj = await Node.init(schema=schema, branch=branch, db=db)
600
609
  await obj.new(**item.to_node(), node=parent, db=db)
601
- await obj.save(db=db, user_id=user_id)
610
+ await obj.save(db=db, at=at, user_id=user_id)
602
611
  new_item = item.duplicate()
603
612
  new_item.id = obj.id
604
613
  return new_item
605
614
 
606
615
  @staticmethod
607
- async def update_attribute_in_db(item: AttributeSchema, attr: Node, db: InfrahubDatabase, user_id: str) -> None:
616
+ async def update_attribute_in_db(
617
+ item: AttributeSchema, attr: Node, db: InfrahubDatabase, at: Timestamp, user_id: str
618
+ ) -> None:
608
619
  item_dict = item.model_dump(exclude={"id", "state", "filters"})
609
620
  for key, value in item_dict.items():
610
621
  getattr(attr, key).value = value
611
- await attr.save(db=db, user_id=user_id)
622
+ await attr.save(db=db, at=at, user_id=user_id)
612
623
 
613
624
  @staticmethod
614
625
  async def create_relationship_in_db(
@@ -618,22 +629,23 @@ class SchemaManager(NodeManager):
618
629
  parent: Node,
619
630
  db: InfrahubDatabase,
620
631
  user_id: str,
632
+ at: Timestamp,
621
633
  ) -> RelationshipSchema:
622
634
  obj = await Node.init(schema=schema, branch=branch, db=db)
623
635
  await obj.new(**item.model_dump(exclude={"id", "state", "filters"}), node=parent, db=db)
624
- await obj.save(db=db, user_id=user_id)
636
+ await obj.save(db=db, at=at, user_id=user_id)
625
637
  new_item = item.duplicate()
626
638
  new_item.id = obj.id
627
639
  return new_item
628
640
 
629
641
  @staticmethod
630
642
  async def update_relationship_in_db(
631
- item: RelationshipSchema, rel: Node, db: InfrahubDatabase, user_id: str
643
+ item: RelationshipSchema, rel: Node, db: InfrahubDatabase, at: Timestamp, user_id: str
632
644
  ) -> None:
633
645
  item_dict = item.model_dump(exclude={"id", "state", "filters"})
634
646
  for key, value in item_dict.items():
635
647
  getattr(rel, key).value = value
636
- await rel.save(db=db, user_id=user_id)
648
+ await rel.save(db=db, at=at, user_id=user_id)
637
649
 
638
650
  async def load_schema(
639
651
  self,
@@ -36,7 +36,7 @@ class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
36
36
  self.params["null_value"] = NULL_VALUE
37
37
 
38
38
  query = """
39
- MATCH p = (n:%(node_kind)s)
39
+ MATCH (n:%(node_kinds)s)
40
40
  CALL (n) {
41
41
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
42
42
  WHERE all(
@@ -51,7 +51,10 @@ class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
51
51
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
52
52
  AND attribute_value IS NOT NULL
53
53
  AND attribute_value <> $null_value
54
- """ % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}
54
+ """ % {
55
+ "branch_filter": branch_filter,
56
+ "node_kinds": f"{self.node_schema.kind}|Profile{self.node_schema.kind}|Template{self.node_schema.kind}",
57
+ }
55
58
 
56
59
  self.add_to_query(query)
57
60
  self.return_labels = ["node.uuid", "attribute_value", "value_relationship.branch as value_branch"]
@@ -85,6 +85,10 @@ class ConstraintValidatorDeterminer:
85
85
  constraints: list[SchemaUpdateConstraintInfo] = []
86
86
  schemas = list(self.schema_branch.get_all(duplicate=False).values())
87
87
  # added here to check their uniqueness constraints
88
+ with contextlib.suppress(SchemaNotFoundError):
89
+ schemas.append(self.schema_branch.get_node(name="SchemaNode", duplicate=False))
90
+ with contextlib.suppress(SchemaNotFoundError):
91
+ schemas.append(self.schema_branch.get_node(name="SchemaGeneric", duplicate=False))
88
92
  with contextlib.suppress(SchemaNotFoundError):
89
93
  schemas.append(self.schema_branch.get_node(name="SchemaAttribute", duplicate=False))
90
94
  with contextlib.suppress(SchemaNotFoundError):
@@ -8,6 +8,7 @@ from functools import cached_property
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from graphql import (
11
+ DocumentNode,
11
12
  FieldNode,
12
13
  FragmentDefinitionNode,
13
14
  FragmentSpreadNode,
@@ -389,6 +390,7 @@ class InfrahubGraphQLQueryAnalyzer(GraphQLQueryAnalyzer):
389
390
  schema: GraphQLSchema | None = None,
390
391
  query_variables: dict[str, Any] | None = None,
391
392
  operation_name: str | None = None,
393
+ document: DocumentNode | None = None,
392
394
  ) -> None:
393
395
  self.branch = branch
394
396
  self.schema_branch = schema_branch
@@ -396,7 +398,7 @@ class InfrahubGraphQLQueryAnalyzer(GraphQLQueryAnalyzer):
396
398
  self.query_variables: dict[str, Any] = query_variables or {}
397
399
  self._named_fragments: dict[str, GraphQLQueryNode] = {}
398
400
  self._fragment_dependencies: dict[str, set[str]] = {}
399
- super().__init__(query=query, schema=schema)
401
+ super().__init__(query=query, schema=schema, document=document)
400
402
 
401
403
  @property
402
404
  def operation_names(self) -> list[str]:
infrahub/graphql/app.py CHANGED
@@ -22,10 +22,7 @@ from graphql import (
22
22
  ExecutionContext,
23
23
  ExecutionResult,
24
24
  GraphQLError,
25
- GraphQLFormattedError,
26
25
  OperationType,
27
- graphql,
28
- parse,
29
26
  subscribe,
30
27
  validate,
31
28
  )
@@ -45,6 +42,7 @@ from infrahub.core.registry import registry
45
42
  from infrahub.core.timestamp import Timestamp
46
43
  from infrahub.exceptions import BranchNotFoundError, Error
47
44
  from infrahub.graphql.analyzer import InfrahubGraphQLQueryAnalyzer
45
+ from infrahub.graphql.execution import cached_parse, execute_graphql_query
48
46
  from infrahub.graphql.initialization import GraphqlParams, prepare_graphql_params
49
47
  from infrahub.log import get_logger
50
48
 
@@ -62,7 +60,7 @@ from .middleware import raise_on_mutation_on_branch_needing_rebase
62
60
 
63
61
  if TYPE_CHECKING:
64
62
  import graphene
65
- from graphql import GraphQLSchema
63
+ from graphql import GraphQLFormattedError, GraphQLSchema
66
64
  from graphql.language.ast import (
67
65
  DocumentNode,
68
66
  OperationDefinitionNode,
@@ -213,6 +211,7 @@ class InfrahubGraphQLApp:
213
211
  schema=graphql_params.schema,
214
212
  operation_name=operation_name,
215
213
  branch=branch,
214
+ document=cached_parse(query),
216
215
  )
217
216
 
218
217
  # if the query contains some mutation, it's not currently supported to set AT manually
@@ -228,6 +227,7 @@ class InfrahubGraphQLApp:
228
227
  schema=graphql_params.schema,
229
228
  operation_name=operation_name,
230
229
  branch=branch,
230
+ document=cached_parse(query),
231
231
  )
232
232
  impacted_models = analyzed_query.query_report.impacted_models
233
233
 
@@ -252,7 +252,7 @@ class InfrahubGraphQLApp:
252
252
  span.set_attributes(labels)
253
253
 
254
254
  with GRAPHQL_DURATION_METRICS.labels(**labels).time():
255
- result = await graphql(
255
+ result = await execute_graphql_query(
256
256
  schema=graphql_params.schema,
257
257
  source=query,
258
258
  context_value=graphql_params.context,
@@ -265,6 +265,7 @@ class InfrahubGraphQLApp:
265
265
 
266
266
  response: dict[str, Any] = {"data": result.data}
267
267
  if result.errors:
268
+ GRAPHQL_QUERY_ERRORS_METRICS.labels(**labels).observe(len(result.errors))
268
269
  for error in result.errors:
269
270
  if error.original_error:
270
271
  self._log_error(error=error.original_error)
@@ -283,10 +284,6 @@ class InfrahubGraphQLApp:
283
284
  GRAPHQL_TOP_LEVEL_QUERIES_METRICS.labels(**labels).observe(analyzed_query.nbr_queries)
284
285
  GRAPHQL_QUERY_OBJECTS_METRICS.labels(**labels).observe(len(impacted_models))
285
286
 
286
- _, errors = analyzed_query.is_valid
287
- if errors:
288
- GRAPHQL_QUERY_ERRORS_METRICS.labels(**labels).observe(len(errors))
289
-
290
287
  return json_response
291
288
 
292
289
  def _set_labels(self, request: Request, branch: Branch, query: InfrahubGraphQLQueryAnalyzer) -> dict[str, Any]: # noqa: ARG002
@@ -391,7 +388,7 @@ class InfrahubGraphQLApp:
391
388
  document: DocumentNode | None = None
392
389
 
393
390
  try:
394
- document = parse(query)
391
+ document = cached_parse(query)
395
392
  operation = get_operation_ast(document, operation_name)
396
393
  errors = validate(graphql_params.schema, document)
397
394
  except GraphQLError as e:
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from inspect import isawaitable
5
+ from typing import TYPE_CHECKING, Any, Callable
6
+
7
+ from graphql import GraphQLSchema, execute, parse, validate
8
+ from graphql.error import GraphQLError
9
+ from graphql.execution import ExecutionResult
10
+ from graphql.type import validate_schema
11
+
12
+ if TYPE_CHECKING:
13
+ from graphql import ExecutionContext, GraphQLFieldResolver, GraphQLTypeResolver
14
+ from graphql.execution import Middleware
15
+ from graphql.language import Source
16
+ from graphql.language.ast import DocumentNode
17
+
18
+
19
+ @lru_cache(maxsize=1024)
20
+ def _cached_parse(query: str) -> DocumentNode:
21
+ """Internal cached parse function for queries without @expand directive."""
22
+ return parse(query)
23
+
24
+
25
+ def cached_parse(query: str | Source) -> DocumentNode:
26
+ """Parse a GraphQL query string into a DocumentNode.
27
+
28
+ Queries containing the @expand directive are not cached because the parser
29
+ mutates the AST to add expanded fields, which would corrupt the cache.
30
+ """
31
+ query_str = query if isinstance(query, str) else query.body
32
+ if "@expand" in query_str:
33
+ return parse(query)
34
+ return _cached_parse(query_str)
35
+
36
+
37
+ @lru_cache(maxsize=1024)
38
+ def cached_validate(schema: GraphQLSchema, document_ast: DocumentNode) -> list[GraphQLError]:
39
+ return validate(schema, document_ast)
40
+
41
+
42
+ @lru_cache(maxsize=1024)
43
+ def cached_validate_schema(schema: GraphQLSchema) -> list[GraphQLError]:
44
+ return validate_schema(schema)
45
+
46
+
47
+ async def execute_graphql_query(
48
+ schema: GraphQLSchema,
49
+ source: str | Source,
50
+ root_value: Any = None,
51
+ context_value: Any = None,
52
+ variable_values: dict[str, Any] | None = None,
53
+ operation_name: str | None = None,
54
+ field_resolver: GraphQLFieldResolver | None = None,
55
+ type_resolver: GraphQLTypeResolver | None = None,
56
+ middleware: Middleware | None = None,
57
+ execution_context_class: type[ExecutionContext] | None = None,
58
+ is_awaitable: Callable[[Any], bool] | None = None,
59
+ ) -> ExecutionResult:
60
+ """Execute a query, return asynchronously only if necessary."""
61
+ # Validate Schema
62
+ schema_validation_errors = cached_validate_schema(schema)
63
+ if schema_validation_errors:
64
+ return ExecutionResult(data=None, errors=schema_validation_errors)
65
+
66
+ # Parse
67
+ try:
68
+ document = cached_parse(source)
69
+ except GraphQLError as error:
70
+ return ExecutionResult(data=None, errors=[error])
71
+
72
+ validation_errors = cached_validate(schema, document)
73
+ if validation_errors:
74
+ return ExecutionResult(data=None, errors=validation_errors)
75
+
76
+ # Execute
77
+ result = execute(
78
+ schema,
79
+ document,
80
+ root_value,
81
+ context_value,
82
+ variable_values,
83
+ operation_name,
84
+ field_resolver,
85
+ type_resolver,
86
+ None,
87
+ middleware,
88
+ execution_context_class,
89
+ is_awaitable,
90
+ )
91
+
92
+ if isawaitable(result):
93
+ return await result
94
+
95
+ return result
@@ -598,7 +598,10 @@ class GraphQLSchemaManager:
598
598
  required=False,
599
599
  description="Human friendly identifier",
600
600
  ),
601
- "_updated_at": graphene.DateTime(required=False),
601
+ "_updated_at": graphene.DateTime(
602
+ required=False,
603
+ deprecation_reason="Query the node_metadata field instead. Will be removed in Infrahub 1.9",
604
+ ),
602
605
  "display_label": graphene.String(required=False),
603
606
  "Meta": type("Meta", (object,), meta_attrs),
604
607
  }
@@ -1209,7 +1212,10 @@ class GraphQLSchemaManager:
1209
1212
 
1210
1213
  main_attrs: dict[str, Any] = {
1211
1214
  "node": graphene.Field(base_interface, required=False),
1212
- "_updated_at": graphene.DateTime(required=False),
1215
+ "_updated_at": graphene.DateTime(
1216
+ required=False,
1217
+ deprecation_reason="Query the node_metadata field instead. Will be removed in Infrahub 1.9",
1218
+ ),
1213
1219
  "node_metadata": graphene.Field(node_metadata, required=True),
1214
1220
  "Meta": type("Meta", (object,), meta_attrs),
1215
1221
  }
@@ -84,7 +84,22 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
84
84
  if state and state != ProposedChangeState.OPEN.value:
85
85
  raise ValidationError(input_value="A proposed change has to be in the open state during creation")
86
86
 
87
+ source_branch_name = data.get("source_branch", {}).get("value")
88
+
87
89
  async with graphql_context.db.start_transaction() as dbt:
90
+ existing_open_pcs = await NodeManager.query(
91
+ db=dbt,
92
+ schema=InfrahubKind.PROPOSEDCHANGE,
93
+ filters={
94
+ "source_branch__value": source_branch_name,
95
+ "state__value": ProposedChangeState.OPEN.value,
96
+ },
97
+ )
98
+ if existing_open_pcs:
99
+ raise ValidationError(
100
+ input_value=f"An open proposed change already exists for branch '{source_branch_name}'"
101
+ )
102
+
88
103
  proposed_change, result = await super().mutate_create(
89
104
  info=info, data=data, branch=branch, database=dbt, override_data=override_data
90
105
  )
@@ -99,12 +99,9 @@ class GraphQLExtractor:
99
99
  key=attribute.name,
100
100
  node=FieldNode(
101
101
  kind="field",
102
- name=NameNode(
103
- kind="name",
104
- value=key,
105
- directives=[],
106
- arguments=[],
107
- ),
102
+ name=NameNode(kind="name", value=key),
103
+ directives=[],
104
+ arguments=[],
108
105
  ),
109
106
  path=attribute_path,
110
107
  fields={key: None},
@@ -117,7 +114,9 @@ class GraphQLExtractor:
117
114
  FieldNode(
118
115
  kind="field",
119
116
  name=NameNode(kind="name", value=attribute.name),
120
- selection_set=SelectionSetNode(selections=tuple(enrichers)),
117
+ selection_set=SelectionSetNode(selections=tuple(e.node for e in enrichers)),
118
+ directives=[],
119
+ arguments=[],
121
120
  )
122
121
  )
123
122
 
@@ -130,6 +129,8 @@ class GraphQLExtractor:
130
129
  kind="field",
131
130
  name=NameNode(kind="name", value="node"),
132
131
  selection_set=SelectionSetNode(selections=tuple(attribute_enrichers)),
132
+ directives=[],
133
+ arguments=[],
133
134
  ),
134
135
  fields={attribute.name: field_attributes for attribute in self.schema.attributes},
135
136
  )
@@ -166,6 +167,8 @@ class GraphQLExtractor:
166
167
  kind="field",
167
168
  name=NameNode(kind="name", value=sub_node.key),
168
169
  selection_set=SelectionSetNode(selections=(sub_node.node,)),
170
+ directives=[],
171
+ arguments=[],
169
172
  )
170
173
  )
171
174
  selection_set.selections = tuple(selections)