infrahub-server 1.6.0b0__py3-none-any.whl → 1.6.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 (91) 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/branch/tasks.py +6 -1
  8. infrahub/core/changelog/models.py +2 -2
  9. infrahub/core/constants/__init__.py +1 -0
  10. infrahub/core/graph/__init__.py +1 -1
  11. infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
  12. infrahub/core/manager.py +36 -31
  13. infrahub/core/migrations/graph/__init__.py +4 -0
  14. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +30 -12
  15. infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +606 -0
  16. infrahub/core/migrations/graph/m048_undelete_rel_props.py +161 -0
  17. infrahub/core/models.py +5 -6
  18. infrahub/core/node/__init__.py +16 -13
  19. infrahub/core/node/create.py +36 -8
  20. infrahub/core/node/proposed_change.py +5 -3
  21. infrahub/core/node/standard.py +1 -1
  22. infrahub/core/protocols.py +1 -7
  23. infrahub/core/query/attribute.py +1 -1
  24. infrahub/core/query/node.py +9 -5
  25. infrahub/core/relationship/model.py +21 -4
  26. infrahub/core/schema/generic_schema.py +1 -1
  27. infrahub/core/schema/manager.py +8 -3
  28. infrahub/core/schema/schema_branch.py +35 -16
  29. infrahub/core/validators/attribute/choices.py +2 -2
  30. infrahub/core/validators/determiner.py +3 -6
  31. infrahub/database/__init__.py +1 -1
  32. infrahub/git/base.py +2 -3
  33. infrahub/git/models.py +13 -0
  34. infrahub/git/tasks.py +23 -19
  35. infrahub/git/utils.py +16 -9
  36. infrahub/graphql/app.py +6 -6
  37. infrahub/graphql/loaders/peers.py +6 -0
  38. infrahub/graphql/mutations/action.py +15 -7
  39. infrahub/graphql/mutations/hfid.py +1 -1
  40. infrahub/graphql/mutations/profile.py +8 -1
  41. infrahub/graphql/mutations/repository.py +3 -3
  42. infrahub/graphql/mutations/schema.py +4 -4
  43. infrahub/graphql/mutations/webhook.py +2 -2
  44. infrahub/graphql/queries/resource_manager.py +2 -3
  45. infrahub/graphql/queries/search.py +2 -3
  46. infrahub/graphql/resolvers/ipam.py +20 -0
  47. infrahub/graphql/resolvers/many_relationship.py +12 -11
  48. infrahub/graphql/resolvers/resolver.py +6 -2
  49. infrahub/graphql/resolvers/single_relationship.py +1 -11
  50. infrahub/log.py +1 -1
  51. infrahub/message_bus/messages/__init__.py +0 -12
  52. infrahub/profiles/node_applier.py +9 -0
  53. infrahub/proposed_change/branch_diff.py +1 -1
  54. infrahub/proposed_change/tasks.py +1 -1
  55. infrahub/repositories/create_repository.py +3 -3
  56. infrahub/task_manager/models.py +1 -1
  57. infrahub/task_manager/task.py +5 -5
  58. infrahub/trigger/setup.py +6 -9
  59. infrahub/utils.py +18 -0
  60. infrahub/validators/tasks.py +1 -1
  61. infrahub/workers/infrahub_async.py +7 -6
  62. infrahub_sdk/client.py +113 -1
  63. infrahub_sdk/ctl/AGENTS.md +67 -0
  64. infrahub_sdk/ctl/branch.py +175 -1
  65. infrahub_sdk/ctl/check.py +3 -3
  66. infrahub_sdk/ctl/cli_commands.py +9 -9
  67. infrahub_sdk/ctl/generator.py +2 -2
  68. infrahub_sdk/ctl/graphql.py +1 -2
  69. infrahub_sdk/ctl/importer.py +1 -2
  70. infrahub_sdk/ctl/repository.py +6 -49
  71. infrahub_sdk/ctl/task.py +2 -4
  72. infrahub_sdk/ctl/utils.py +2 -2
  73. infrahub_sdk/ctl/validate.py +1 -2
  74. infrahub_sdk/diff.py +80 -3
  75. infrahub_sdk/graphql/constants.py +14 -1
  76. infrahub_sdk/graphql/renderers.py +5 -1
  77. infrahub_sdk/node/attribute.py +0 -1
  78. infrahub_sdk/node/constants.py +3 -1
  79. infrahub_sdk/node/node.py +303 -3
  80. infrahub_sdk/node/related_node.py +1 -2
  81. infrahub_sdk/node/relationship.py +1 -2
  82. infrahub_sdk/protocols_base.py +0 -1
  83. infrahub_sdk/pytest_plugin/AGENTS.md +67 -0
  84. infrahub_sdk/schema/__init__.py +0 -3
  85. infrahub_sdk/timestamp.py +7 -7
  86. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/METADATA +2 -3
  87. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/RECORD +91 -86
  88. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/WHEEL +1 -1
  89. infrahub_testcontainers/container.py +2 -2
  90. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/entry_points.txt +0 -0
  91. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/licenses/LICENSE.txt +0 -0
infrahub_sdk/node/node.py CHANGED
@@ -7,7 +7,14 @@ from typing import TYPE_CHECKING, Any
7
7
  from ..constants import InfrahubClientMode
8
8
  from ..exceptions import FeatureNotSupportedError, NodeNotFoundError, ResourceNotDefinedError, SchemaNotFoundError
9
9
  from ..graphql import Mutation, Query
10
- from ..schema import GenericSchemaAPI, RelationshipCardinality, RelationshipKind
10
+ from ..schema import (
11
+ GenericSchemaAPI,
12
+ ProfileSchemaAPI,
13
+ RelationshipCardinality,
14
+ RelationshipKind,
15
+ RelationshipSchemaAPI,
16
+ TemplateSchemaAPI,
17
+ )
11
18
  from ..utils import compare_lists, generate_short_id
12
19
  from .attribute import Attribute
13
20
  from .constants import (
@@ -56,9 +63,20 @@ class InfrahubNodeBase:
56
63
  self._attributes = [item.name for item in self._schema.attributes]
57
64
  self._relationships = [item.name for item in self._schema.relationships]
58
65
 
59
- self._artifact_support = hasattr(schema, "inherit_from") and "CoreArtifactTarget" in schema.inherit_from
66
+ # GenericSchemaAPI doesn't have inherit_from, so we need to check the type first
67
+ if isinstance(schema, GenericSchemaAPI):
68
+ self._artifact_support = False
69
+ else:
70
+ inherit_from = getattr(schema, "inherit_from", None) or []
71
+ self._artifact_support = "CoreArtifactTarget" in inherit_from
60
72
  self._artifact_definition_support = schema.kind == "CoreArtifactDefinition"
61
73
 
74
+ # Check if this node is hierarchical (supports parent/children and ancestors/descendants)
75
+ if not isinstance(schema, (ProfileSchemaAPI, GenericSchemaAPI, TemplateSchemaAPI)):
76
+ self._hierarchy_support = getattr(schema, "hierarchy", None) is not None
77
+ else:
78
+ self._hierarchy_support = False
79
+
62
80
  if not self.id:
63
81
  self._existing = False
64
82
 
@@ -384,6 +402,10 @@ class InfrahubNodeBase:
384
402
  if not self._artifact_support:
385
403
  raise FeatureNotSupportedError(message)
386
404
 
405
+ def _validate_hierarchy_support(self, message: str) -> None:
406
+ if not self._hierarchy_support:
407
+ raise FeatureNotSupportedError(message)
408
+
387
409
  def _validate_artifact_definition_support(self, message: str) -> None:
388
410
  if not self._artifact_definition_support:
389
411
  raise FeatureNotSupportedError(message)
@@ -479,6 +501,7 @@ class InfrahubNode(InfrahubNodeBase):
479
501
 
480
502
  self._relationship_cardinality_many_data: dict[str, RelationshipManager] = {}
481
503
  self._relationship_cardinality_one_data: dict[str, RelatedNode] = {}
504
+ self._hierarchical_data: dict[str, RelatedNode | RelationshipManager] = {}
482
505
 
483
506
  super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
484
507
 
@@ -532,6 +555,75 @@ class InfrahubNode(InfrahubNodeBase):
532
555
  schema=rel_schema,
533
556
  data=rel_data,
534
557
  )
558
+ # Initialize parent, children, ancestors and descendants for hierarchical nodes
559
+ if self._hierarchy_support:
560
+ # Create pseudo-schema for parent (cardinality one)
561
+ parent_schema = RelationshipSchemaAPI(
562
+ name="parent",
563
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
564
+ kind=RelationshipKind.HIERARCHY,
565
+ cardinality="one",
566
+ optional=True,
567
+ )
568
+ parent_data = data.get("parent", None) if isinstance(data, dict) else None
569
+ self._hierarchical_data["parent"] = RelatedNode(
570
+ name="parent",
571
+ client=self._client,
572
+ branch=self._branch,
573
+ schema=parent_schema,
574
+ data=parent_data,
575
+ )
576
+ # Create pseudo-schema for children (many cardinality)
577
+ children_schema = RelationshipSchemaAPI(
578
+ name="children",
579
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
580
+ kind=RelationshipKind.HIERARCHY,
581
+ cardinality="many",
582
+ optional=True,
583
+ )
584
+ children_data = data.get("children", None) if isinstance(data, dict) else None
585
+ self._hierarchical_data["children"] = RelationshipManager(
586
+ name="children",
587
+ client=self._client,
588
+ node=self,
589
+ branch=self._branch,
590
+ schema=children_schema,
591
+ data=children_data,
592
+ )
593
+ # Create pseudo-schema for ancestors (read-only, many cardinality)
594
+ ancestors_schema = RelationshipSchemaAPI(
595
+ name="ancestors",
596
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
597
+ cardinality="many",
598
+ read_only=True,
599
+ optional=True,
600
+ )
601
+ ancestors_data = data.get("ancestors", None) if isinstance(data, dict) else None
602
+ self._hierarchical_data["ancestors"] = RelationshipManager(
603
+ name="ancestors",
604
+ client=self._client,
605
+ node=self,
606
+ branch=self._branch,
607
+ schema=ancestors_schema,
608
+ data=ancestors_data,
609
+ )
610
+ # Create pseudo-schema for descendants (read-only, many cardinality)
611
+ descendants_schema = RelationshipSchemaAPI(
612
+ name="descendants",
613
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
614
+ cardinality="many",
615
+ read_only=True,
616
+ optional=True,
617
+ )
618
+ descendants_data = data.get("descendants", None) if isinstance(data, dict) else None
619
+ self._hierarchical_data["descendants"] = RelationshipManager(
620
+ name="descendants",
621
+ client=self._client,
622
+ node=self,
623
+ branch=self._branch,
624
+ schema=descendants_schema,
625
+ data=descendants_data,
626
+ )
535
627
 
536
628
  def __getattr__(self, name: str) -> Attribute | RelationshipManager | RelatedNode:
537
629
  if "_attribute_data" in self.__dict__ and name in self._attribute_data:
@@ -540,6 +632,8 @@ class InfrahubNode(InfrahubNodeBase):
540
632
  return self._relationship_cardinality_many_data[name]
541
633
  if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
542
634
  return self._relationship_cardinality_one_data[name]
635
+ if "_hierarchical_data" in self.__dict__ and name in self._hierarchical_data:
636
+ return self._hierarchical_data[name]
543
637
 
544
638
  raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
545
639
 
@@ -628,6 +722,57 @@ class InfrahubNode(InfrahubNodeBase):
628
722
 
629
723
  self._client.store.set(node=self)
630
724
 
725
+ async def _process_hierarchical_fields(
726
+ self,
727
+ data: dict[str, Any],
728
+ include: list[str] | None = None,
729
+ exclude: list[str] | None = None,
730
+ prefetch_relationships: bool = False,
731
+ insert_alias: bool = False,
732
+ property: bool = False,
733
+ ) -> None:
734
+ """Process hierarchical fields (parent, children, ancestors, descendants) for hierarchical nodes."""
735
+ if not self._hierarchy_support:
736
+ return
737
+
738
+ for hierarchical_name in ["parent", "children", "ancestors", "descendants"]:
739
+ if exclude and hierarchical_name in exclude:
740
+ continue
741
+
742
+ # Only include if explicitly requested or if prefetch_relationships is True
743
+ should_fetch = prefetch_relationships or (include is not None and hierarchical_name in include)
744
+ if not should_fetch:
745
+ continue
746
+
747
+ peer_schema = await self._client.schema.get(kind=self._schema.hierarchy, branch=self._branch) # type: ignore[union-attr, arg-type]
748
+ peer_node = InfrahubNode(client=self._client, schema=peer_schema, branch=self._branch)
749
+ # Exclude hierarchical fields from peer data to prevent infinite recursion
750
+ peer_exclude = list(exclude) if exclude else []
751
+ peer_exclude.extend(["parent", "children", "ancestors", "descendants"])
752
+ peer_data = await peer_node.generate_query_data_node(
753
+ exclude=peer_exclude,
754
+ property=property,
755
+ )
756
+
757
+ # Parent is cardinality one, others are cardinality many
758
+ if hierarchical_name == "parent":
759
+ hierarchical_data = RelatedNode._generate_query_data(peer_data=peer_data, property=property)
760
+ # Use fragment for hierarchical fields similar to hierarchy relationships
761
+ data_node = hierarchical_data["node"]
762
+ hierarchical_data["node"] = {}
763
+ hierarchical_data["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
764
+ else:
765
+ hierarchical_data = RelationshipManager._generate_query_data(peer_data=peer_data, property=property)
766
+ # Use fragment for hierarchical fields similar to hierarchy relationships
767
+ data_node = hierarchical_data["edges"]["node"]
768
+ hierarchical_data["edges"]["node"] = {}
769
+ hierarchical_data["edges"]["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
770
+
771
+ data[hierarchical_name] = hierarchical_data
772
+
773
+ if insert_alias:
774
+ data[hierarchical_name]["@alias"] = f"__alias__{self._schema.kind}__{hierarchical_name}"
775
+
631
776
  async def generate_query_data(
632
777
  self,
633
778
  filters: dict[str, Any] | None = None,
@@ -755,6 +900,7 @@ class InfrahubNode(InfrahubNodeBase):
755
900
  property=property,
756
901
  )
757
902
 
903
+ rel_data: dict[str, Any]
758
904
  if rel_schema and rel_schema.cardinality == "one":
759
905
  rel_data = RelatedNode._generate_query_data(peer_data=peer_data, property=property)
760
906
  # Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
@@ -767,12 +913,24 @@ class InfrahubNode(InfrahubNodeBase):
767
913
  rel_data["node"][f"...on {rel_schema.peer}"] = data_node
768
914
  elif rel_schema and rel_schema.cardinality == "many":
769
915
  rel_data = RelationshipManager._generate_query_data(peer_data=peer_data, property=property)
916
+ else:
917
+ continue
770
918
 
771
919
  data[rel_name] = rel_data
772
920
 
773
921
  if insert_alias:
774
922
  data[rel_name]["@alias"] = f"__alias__{self._schema.kind}__{rel_name}"
775
923
 
924
+ # Add parent, children, ancestors and descendants for hierarchical nodes
925
+ await self._process_hierarchical_fields(
926
+ data=data,
927
+ include=include,
928
+ exclude=exclude,
929
+ prefetch_relationships=prefetch_relationships,
930
+ insert_alias=insert_alias,
931
+ property=property,
932
+ )
933
+
776
934
  return data
777
935
 
778
936
  async def add_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None:
@@ -1132,6 +1290,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1132
1290
 
1133
1291
  self._relationship_cardinality_many_data: dict[str, RelationshipManagerSync] = {}
1134
1292
  self._relationship_cardinality_one_data: dict[str, RelatedNodeSync] = {}
1293
+ self._hierarchical_data: dict[str, RelatedNodeSync | RelationshipManagerSync] = {}
1135
1294
 
1136
1295
  super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
1137
1296
 
@@ -1186,6 +1345,79 @@ class InfrahubNodeSync(InfrahubNodeBase):
1186
1345
  data=rel_data,
1187
1346
  )
1188
1347
 
1348
+ # Initialize parent, children, ancestors and descendants for hierarchical nodes
1349
+ if self._hierarchy_support:
1350
+ # Create pseudo-schema for parent (cardinality one)
1351
+ parent_schema = RelationshipSchemaAPI(
1352
+ name="parent",
1353
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
1354
+ kind=RelationshipKind.HIERARCHY,
1355
+ cardinality="one",
1356
+ optional=True,
1357
+ )
1358
+ parent_data = data.get("parent", None) if isinstance(data, dict) else None
1359
+ self._hierarchical_data["parent"] = RelatedNodeSync(
1360
+ name="parent",
1361
+ client=self._client,
1362
+ branch=self._branch,
1363
+ schema=parent_schema,
1364
+ data=parent_data,
1365
+ )
1366
+
1367
+ # Create pseudo-schema for children (many cardinality)
1368
+ children_schema = RelationshipSchemaAPI(
1369
+ name="children",
1370
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
1371
+ kind=RelationshipKind.HIERARCHY,
1372
+ cardinality="many",
1373
+ optional=True,
1374
+ )
1375
+ children_data = data.get("children", None) if isinstance(data, dict) else None
1376
+ self._hierarchical_data["children"] = RelationshipManagerSync(
1377
+ name="children",
1378
+ client=self._client,
1379
+ node=self,
1380
+ branch=self._branch,
1381
+ schema=children_schema,
1382
+ data=children_data,
1383
+ )
1384
+
1385
+ # Create pseudo-schema for ancestors (read-only, many cardinality)
1386
+ ancestors_schema = RelationshipSchemaAPI(
1387
+ name="ancestors",
1388
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
1389
+ cardinality="many",
1390
+ read_only=True,
1391
+ optional=True,
1392
+ )
1393
+ ancestors_data = data.get("ancestors", None) if isinstance(data, dict) else None
1394
+ self._hierarchical_data["ancestors"] = RelationshipManagerSync(
1395
+ name="ancestors",
1396
+ client=self._client,
1397
+ node=self,
1398
+ branch=self._branch,
1399
+ schema=ancestors_schema,
1400
+ data=ancestors_data,
1401
+ )
1402
+
1403
+ # Create pseudo-schema for descendants (read-only, many cardinality)
1404
+ descendants_schema = RelationshipSchemaAPI(
1405
+ name="descendants",
1406
+ peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
1407
+ cardinality="many",
1408
+ read_only=True,
1409
+ optional=True,
1410
+ )
1411
+ descendants_data = data.get("descendants", None) if isinstance(data, dict) else None
1412
+ self._hierarchical_data["descendants"] = RelationshipManagerSync(
1413
+ name="descendants",
1414
+ client=self._client,
1415
+ node=self,
1416
+ branch=self._branch,
1417
+ schema=descendants_schema,
1418
+ data=descendants_data,
1419
+ )
1420
+
1189
1421
  def __getattr__(self, name: str) -> Attribute | RelationshipManagerSync | RelatedNodeSync:
1190
1422
  if "_attribute_data" in self.__dict__ and name in self._attribute_data:
1191
1423
  return self._attribute_data[name]
@@ -1193,6 +1425,8 @@ class InfrahubNodeSync(InfrahubNodeBase):
1193
1425
  return self._relationship_cardinality_many_data[name]
1194
1426
  if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
1195
1427
  return self._relationship_cardinality_one_data[name]
1428
+ if "_hierarchical_data" in self.__dict__ and name in self._hierarchical_data:
1429
+ return self._hierarchical_data[name]
1196
1430
 
1197
1431
  raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
1198
1432
 
@@ -1274,6 +1508,57 @@ class InfrahubNodeSync(InfrahubNodeBase):
1274
1508
 
1275
1509
  self._client.store.set(node=self)
1276
1510
 
1511
+ def _process_hierarchical_fields(
1512
+ self,
1513
+ data: dict[str, Any],
1514
+ include: list[str] | None = None,
1515
+ exclude: list[str] | None = None,
1516
+ prefetch_relationships: bool = False,
1517
+ insert_alias: bool = False,
1518
+ property: bool = False,
1519
+ ) -> None:
1520
+ """Process hierarchical fields (parent, children, ancestors, descendants) for hierarchical nodes."""
1521
+ if not self._hierarchy_support:
1522
+ return
1523
+
1524
+ for hierarchical_name in ["parent", "children", "ancestors", "descendants"]:
1525
+ if exclude and hierarchical_name in exclude:
1526
+ continue
1527
+
1528
+ # Only include if explicitly requested or if prefetch_relationships is True
1529
+ should_fetch = prefetch_relationships or (include is not None and hierarchical_name in include)
1530
+ if not should_fetch:
1531
+ continue
1532
+
1533
+ peer_schema = self._client.schema.get(kind=self._schema.hierarchy, branch=self._branch) # type: ignore[union-attr, arg-type]
1534
+ peer_node = InfrahubNodeSync(client=self._client, schema=peer_schema, branch=self._branch)
1535
+ # Exclude hierarchical fields from peer data to prevent infinite recursion
1536
+ peer_exclude = list(exclude) if exclude else []
1537
+ peer_exclude.extend(["parent", "children", "ancestors", "descendants"])
1538
+ peer_data = peer_node.generate_query_data_node(
1539
+ exclude=peer_exclude,
1540
+ property=property,
1541
+ )
1542
+
1543
+ # Parent is cardinality one, others are cardinality many
1544
+ if hierarchical_name == "parent":
1545
+ hierarchical_data = RelatedNodeSync._generate_query_data(peer_data=peer_data, property=property)
1546
+ # Use fragment for hierarchical fields similar to hierarchy relationships
1547
+ data_node = hierarchical_data["node"]
1548
+ hierarchical_data["node"] = {}
1549
+ hierarchical_data["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
1550
+ else:
1551
+ hierarchical_data = RelationshipManagerSync._generate_query_data(peer_data=peer_data, property=property)
1552
+ # Use fragment for hierarchical fields similar to hierarchy relationships
1553
+ data_node = hierarchical_data["edges"]["node"]
1554
+ hierarchical_data["edges"]["node"] = {}
1555
+ hierarchical_data["edges"]["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
1556
+
1557
+ data[hierarchical_name] = hierarchical_data
1558
+
1559
+ if insert_alias:
1560
+ data[hierarchical_name]["@alias"] = f"__alias__{self._schema.kind}__{hierarchical_name}"
1561
+
1277
1562
  def generate_query_data(
1278
1563
  self,
1279
1564
  filters: dict[str, Any] | None = None,
@@ -1396,8 +1681,11 @@ class InfrahubNodeSync(InfrahubNodeBase):
1396
1681
  if rel_schema and should_fetch_relationship:
1397
1682
  peer_schema = self._client.schema.get(kind=rel_schema.peer, branch=self._branch)
1398
1683
  peer_node = InfrahubNodeSync(client=self._client, schema=peer_schema, branch=self._branch)
1399
- peer_data = peer_node.generate_query_data_node(include=include, exclude=exclude, property=property)
1684
+ peer_data = peer_node.generate_query_data_node(
1685
+ property=property,
1686
+ )
1400
1687
 
1688
+ rel_data: dict[str, Any]
1401
1689
  if rel_schema and rel_schema.cardinality == "one":
1402
1690
  rel_data = RelatedNodeSync._generate_query_data(peer_data=peer_data, property=property)
1403
1691
  # Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
@@ -1410,12 +1698,24 @@ class InfrahubNodeSync(InfrahubNodeBase):
1410
1698
  rel_data["node"][f"...on {rel_schema.peer}"] = data_node
1411
1699
  elif rel_schema and rel_schema.cardinality == "many":
1412
1700
  rel_data = RelationshipManagerSync._generate_query_data(peer_data=peer_data, property=property)
1701
+ else:
1702
+ continue
1413
1703
 
1414
1704
  data[rel_name] = rel_data
1415
1705
 
1416
1706
  if insert_alias:
1417
1707
  data[rel_name]["@alias"] = f"__alias__{self._schema.kind}__{rel_name}"
1418
1708
 
1709
+ # Add parent, children, ancestors and descendants for hierarchical nodes
1710
+ self._process_hierarchical_fields(
1711
+ data=data,
1712
+ include=include,
1713
+ exclude=exclude,
1714
+ prefetch_relationships=prefetch_relationships,
1715
+ insert_alias=insert_alias,
1716
+ property=property,
1717
+ )
1718
+
1419
1719
  return data
1420
1720
 
1421
1721
  def add_relationships(
@@ -64,7 +64,7 @@ class RelatedNodeBase:
64
64
  self._display_label = node_data.get("display_label", None)
65
65
  self._typename = node_data.get("__typename", None)
66
66
 
67
- self.updated_at: str | None = data.get("updated_at", data.get("_relation__updated_at"))
67
+ self.updated_at: str | None = data.get("updated_at", properties_data.get("updated_at", None))
68
68
 
69
69
  # FIXME, we won't need that once we are only supporting paginated results
70
70
  if self._typename and self._typename.startswith("Related"):
@@ -171,7 +171,6 @@ class RelatedNodeBase:
171
171
  for prop_name in PROPERTIES_OBJECT:
172
172
  properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
173
173
 
174
- if properties:
175
174
  data["properties"] = properties
176
175
  if peer_data:
177
176
  data["node"].update(peer_data)
@@ -87,9 +87,8 @@ class RelationshipManagerBase:
87
87
  properties[prop_name] = None
88
88
  for prop_name in PROPERTIES_OBJECT:
89
89
  properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
90
-
91
- if properties:
92
90
  data["edges"]["properties"] = properties
91
+
93
92
  if peer_data:
94
93
  data["edges"]["node"].update(peer_data)
95
94
 
@@ -56,7 +56,6 @@ class Attribute(Protocol):
56
56
  is_from_profile: bool | None
57
57
  is_inherited: bool | None
58
58
  updated_at: str | None
59
- is_visible: bool | None
60
59
  is_protected: bool | None
61
60
 
62
61
 
@@ -0,0 +1,67 @@
1
+ # infrahub_sdk/pytest_plugin/AGENTS.md
2
+
3
+ Custom pytest plugin for testing Infrahub resources via YAML test files.
4
+
5
+ ## YAML Test Format
6
+
7
+ ```yaml
8
+ infrahub_tests:
9
+ - resource: Check # Check, GraphQLQuery, Jinja2Transform, PythonTransform
10
+ resource_name: my_check
11
+ tests:
12
+ - name: test_success_case
13
+ spec:
14
+ kind: check-smoke # See test kinds below
15
+ input:
16
+ data: {...}
17
+ output:
18
+ passed: true
19
+ ```
20
+
21
+ ## Test Kinds
22
+
23
+ | Resource | Smoke | Unit | Integration |
24
+ | -------- | ----- | ---- | ----------- |
25
+ | Check | `check-smoke` | `check-unit-process` | `check-integration` |
26
+ | GraphQL | `graphql-query-smoke` | - | `graphql-query-integration` |
27
+ | Jinja2 | `jinja2-transform-smoke` | `jinja2-transform-unit-render` | `jinja2-transform-integration` |
28
+ | Python | `python-transform-smoke` | `python-transform-unit-process` | `python-transform-integration` |
29
+
30
+ ## Plugin Structure
31
+
32
+ ```text
33
+ infrahub_sdk/pytest_plugin/
34
+ ├── plugin.py # Pytest hooks (pytest_collect_file, etc.)
35
+ ├── loader.py # YAML loading, ITEMS_MAPPING
36
+ ├── models.py # Pydantic schemas for test files
37
+ └── items/ # Test item implementations
38
+ ├── base.py # InfrahubItem base class
39
+ └── check.py # Check-specific items
40
+ ```
41
+
42
+ ## Adding New Test Item
43
+
44
+ ```python
45
+ # 1. Create item class in items/
46
+ class MyCustomItem(InfrahubItem):
47
+ def runtest(self):
48
+ result = self.process(self.test.input)
49
+ assert result == self.test.output
50
+
51
+ # 2. Register in loader.py
52
+ ITEMS_MAPPING = {
53
+ "my-custom-test": MyCustomItem,
54
+ ...
55
+ }
56
+ ```
57
+
58
+ ## Boundaries
59
+
60
+ ✅ **Always**
61
+
62
+ - Register new items in `ITEMS_MAPPING`
63
+ - Inherit from `InfrahubItem` base class
64
+
65
+ 🚫 **Never**
66
+
67
+ - Forget to add new test kinds to `ITEMS_MAPPING`
@@ -154,7 +154,6 @@ class InfrahubSchemaBase:
154
154
  source: str | None = None,
155
155
  owner: str | None = None,
156
156
  is_protected: bool | None = None,
157
- is_visible: bool | None = None,
158
157
  ) -> dict[str, Any]:
159
158
  obj_data: dict[str, Any] = {}
160
159
  item_metadata: dict[str, Any] = {}
@@ -164,8 +163,6 @@ class InfrahubSchemaBase:
164
163
  item_metadata["owner"] = str(owner)
165
164
  if is_protected is not None:
166
165
  item_metadata["is_protected"] = is_protected
167
- if is_visible is not None:
168
- item_metadata["is_visible"] = is_visible
169
166
 
170
167
  for key, value in data.items():
171
168
  obj_data[key] = {}
infrahub_sdk/timestamp.py CHANGED
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
6
6
  from typing import Literal, TypedDict
7
7
 
8
8
  from typing_extensions import NotRequired
9
- from whenever import Date, Instant, LocalDateTime, OffsetDateTime, Time, ZonedDateTime
9
+ from whenever import Date, Instant, OffsetDateTime, PlainDateTime, Time, ZonedDateTime
10
10
 
11
11
  from .exceptions import TimestampFormatError
12
12
 
@@ -51,30 +51,30 @@ class Timestamp:
51
51
  @classmethod
52
52
  def _parse_string(cls, value: str) -> ZonedDateTime:
53
53
  try:
54
- return ZonedDateTime.parse_common_iso(value)
54
+ return ZonedDateTime.parse_iso(value)
55
55
  except ValueError:
56
56
  pass
57
57
 
58
58
  try:
59
- instant_date = Instant.parse_common_iso(value)
59
+ instant_date = Instant.parse_iso(value)
60
60
  return instant_date.to_tz("UTC")
61
61
  except ValueError:
62
62
  pass
63
63
 
64
64
  try:
65
- local_date_time = LocalDateTime.parse_common_iso(value)
66
- return local_date_time.assume_utc().to_tz("UTC")
65
+ plain_date_time = PlainDateTime.parse_iso(value)
66
+ return plain_date_time.assume_utc().to_tz("UTC")
67
67
  except ValueError:
68
68
  pass
69
69
 
70
70
  try:
71
- offset_date_time = OffsetDateTime.parse_common_iso(value)
71
+ offset_date_time = OffsetDateTime.parse_iso(value)
72
72
  return offset_date_time.to_tz("UTC")
73
73
  except ValueError:
74
74
  pass
75
75
 
76
76
  try:
77
- date = Date.parse_common_iso(value)
77
+ date = Date.parse_iso(value)
78
78
  local_date = date.at(Time(12, 00))
79
79
  return local_date.assume_tz("UTC", disambiguate="compatible")
80
80
  except ValueError:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: infrahub-server
3
- Version: 1.6.0b0
3
+ Version: 1.6.2
4
4
  Summary: Infrahub is taking a new approach to Infrastructure Management by providing a new generation of datastore to organize and control all the data that defines how an infrastructure should run.
5
5
  Project-URL: Homepage, https://opsmill.com
6
6
  Project-URL: Repository, https://github.com/opsmill/infrahub
@@ -21,7 +21,6 @@ Requires-Dist: bcrypt<4.2,>=4.1
21
21
  Requires-Dist: boto3==1.34.129
22
22
  Requires-Dist: cachetools-async==0.0.5
23
23
  Requires-Dist: click==8.1.7
24
- Requires-Dist: copier==9.8.0
25
24
  Requires-Dist: deepdiff==8.6.1
26
25
  Requires-Dist: dulwich==0.22.7
27
26
  Requires-Dist: email-validator<2.2,>=2.1
@@ -60,7 +59,7 @@ Requires-Dist: tomli>=1.1.0; python_version <= '3.11'
60
59
  Requires-Dist: typer==0.19.2
61
60
  Requires-Dist: ujson<6,>=5
62
61
  Requires-Dist: uvicorn[standard]<0.33,>=0.32
63
- Requires-Dist: whenever==0.7.3
62
+ Requires-Dist: whenever==0.9.3
64
63
  Description-Content-Type: text/markdown
65
64
 
66
65
  <h1 align="center">