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
infrahub_sdk/node/node.py CHANGED
@@ -23,6 +23,7 @@ from .constants import (
23
23
  ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE,
24
24
  PROPERTIES_OBJECT,
25
25
  )
26
+ from .metadata import NodeMetadata
26
27
  from .related_node import RelatedNode, RelatedNodeBase, RelatedNodeSync
27
28
  from .relationship import RelationshipManager, RelationshipManagerBase, RelationshipManagerSync
28
29
 
@@ -50,6 +51,7 @@ class InfrahubNodeBase:
50
51
  self._branch = branch
51
52
  self._existing: bool = True
52
53
  self._attribute_data: dict[str, Attribute] = {}
54
+ self._metadata: NodeMetadata | None = None
53
55
 
54
56
  # Generate a unique ID only to be used inside the SDK
55
57
  # The format if this ID is purposely different from the ID used by the API
@@ -152,6 +154,10 @@ class InfrahubNodeBase:
152
154
  def hfid_str(self) -> str | None:
153
155
  return self.get_human_friendly_id_as_string(include_kind=True)
154
156
 
157
+ def get_node_metadata(self) -> NodeMetadata | None:
158
+ """Returns the node metadata (created_at, created_by, updated_at, updated_by) if fetched."""
159
+ return self._metadata
160
+
155
161
  def _init_attributes(self, data: dict | None = None) -> None:
156
162
  for attr_schema in self._schema.attributes:
157
163
  attr_data = data.get(attr_schema.name, None) if isinstance(data, dict) else None
@@ -192,8 +198,8 @@ class InfrahubNodeBase:
192
198
  return self._schema.kind
193
199
 
194
200
  def get_all_kinds(self) -> list[str]:
195
- if hasattr(self._schema, "inherit_from"):
196
- return [self._schema.kind] + self._schema.inherit_from
201
+ if inherit_from := getattr(self._schema, "inherit_from", None):
202
+ return [self._schema.kind] + inherit_from
197
203
  return [self._schema.kind]
198
204
 
199
205
  def is_ip_prefix(self) -> bool:
@@ -210,7 +216,7 @@ class InfrahubNodeBase:
210
216
  def get_raw_graphql_data(self) -> dict | None:
211
217
  return self._data
212
218
 
213
- def _generate_input_data( # noqa: C901
219
+ def _generate_input_data( # noqa: C901, PLR0915
214
220
  self,
215
221
  exclude_unmodified: bool = False,
216
222
  exclude_hfid: bool = False,
@@ -253,7 +259,10 @@ class InfrahubNodeBase:
253
259
  rel: RelatedNodeBase | RelationshipManagerBase = getattr(self, item_name)
254
260
 
255
261
  if rel_schema.cardinality == RelationshipCardinality.ONE and rel_schema.optional and not rel.initialized:
256
- data[item_name] = None
262
+ # Only include None for existing nodes to allow clearing relationships
263
+ # For new nodes, omit the field to allow object template defaults to be applied
264
+ if self._existing:
265
+ data[item_name] = None
257
266
  continue
258
267
 
259
268
  if rel is None or not rel.initialized:
@@ -419,12 +428,16 @@ class InfrahubNodeBase:
419
428
  exclude: list[str] | None = None,
420
429
  partial_match: bool = False,
421
430
  order: Order | None = None,
431
+ include_metadata: bool = False,
422
432
  ) -> dict[str, Any | dict]:
423
433
  data: dict[str, Any] = {
424
434
  "count": None,
425
435
  "edges": {"node": {"id": None, "hfid": None, "display_label": None, "__typename": None}},
426
436
  }
427
437
 
438
+ if include_metadata:
439
+ data["edges"]["node_metadata"] = NodeMetadata._generate_query_data()
440
+
428
441
  data["@filters"] = deepcopy(filters) if filters is not None else {}
429
442
 
430
443
  if order:
@@ -496,8 +509,12 @@ class InfrahubNode(InfrahubNodeBase):
496
509
  """
497
510
  self._client = client
498
511
 
499
- if isinstance(data, dict) and isinstance(data.get("node"), dict):
500
- data = data.get("node")
512
+ # Extract node_metadata before extracting node data (node_metadata is sibling to node in edges)
513
+ node_metadata_data: dict | None = None
514
+ if isinstance(data, dict):
515
+ node_metadata_data = data.get("node_metadata")
516
+ if isinstance(data.get("node"), dict):
517
+ data = data.get("node")
501
518
 
502
519
  self._relationship_cardinality_many_data: dict[str, RelationshipManager] = {}
503
520
  self._relationship_cardinality_one_data: dict[str, RelatedNode] = {}
@@ -505,6 +522,10 @@ class InfrahubNode(InfrahubNodeBase):
505
522
 
506
523
  super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
507
524
 
525
+ # Initialize metadata after base class init
526
+ if node_metadata_data:
527
+ self._metadata = NodeMetadata(node_metadata_data)
528
+
508
529
  @classmethod
509
530
  async def from_graphql(
510
531
  cls,
@@ -785,6 +806,7 @@ class InfrahubNode(InfrahubNodeBase):
785
806
  partial_match: bool = False,
786
807
  property: bool = False,
787
808
  order: Order | None = None,
809
+ include_metadata: bool = False,
788
810
  ) -> dict[str, Any | dict]:
789
811
  data = self.generate_query_data_init(
790
812
  filters=filters,
@@ -794,6 +816,7 @@ class InfrahubNode(InfrahubNodeBase):
794
816
  exclude=exclude,
795
817
  partial_match=partial_match,
796
818
  order=order,
819
+ include_metadata=include_metadata,
797
820
  )
798
821
  data["edges"]["node"].update(
799
822
  await self.generate_query_data_node(
@@ -802,6 +825,7 @@ class InfrahubNode(InfrahubNodeBase):
802
825
  prefetch_relationships=prefetch_relationships,
803
826
  inherited=True,
804
827
  property=property,
828
+ include_metadata=include_metadata,
805
829
  )
806
830
  )
807
831
 
@@ -825,6 +849,7 @@ class InfrahubNode(InfrahubNodeBase):
825
849
  inherited=False,
826
850
  insert_alias=True,
827
851
  property=property,
852
+ include_metadata=include_metadata,
828
853
  )
829
854
 
830
855
  if child_data:
@@ -840,6 +865,7 @@ class InfrahubNode(InfrahubNodeBase):
840
865
  insert_alias: bool = False,
841
866
  prefetch_relationships: bool = False,
842
867
  property: bool = False,
868
+ include_metadata: bool = False,
843
869
  ) -> dict[str, Any | dict]:
844
870
  """Generate the node part of a GraphQL Query with attributes and nodes.
845
871
 
@@ -850,6 +876,7 @@ class InfrahubNode(InfrahubNodeBase):
850
876
  Defaults to True.
851
877
  insert_alias (bool, optional): If True, inserts aliases in the query for each attribute or relationship.
852
878
  prefetch_relationships (bool, optional): If True, pre-fetches relationship data as part of the query.
879
+ include_metadata (bool, optional): If True, includes node_metadata and relationship_metadata in the query.
853
880
 
854
881
  Returns:
855
882
  dict[str, Union[Any, Dict]]: GraphQL query in dictionary format
@@ -866,7 +893,7 @@ class InfrahubNode(InfrahubNodeBase):
866
893
  if not inherited and attr._schema.inherited:
867
894
  continue
868
895
 
869
- attr_data = attr._generate_query_data(property=property)
896
+ attr_data = attr._generate_query_data(property=property, include_metadata=include_metadata)
870
897
  if attr_data:
871
898
  data[attr_name] = attr_data
872
899
  if insert_alias:
@@ -898,11 +925,14 @@ class InfrahubNode(InfrahubNodeBase):
898
925
  peer_node = InfrahubNode(client=self._client, schema=peer_schema, branch=self._branch)
899
926
  peer_data = await peer_node.generate_query_data_node(
900
927
  property=property,
928
+ include_metadata=include_metadata,
901
929
  )
902
930
 
903
931
  rel_data: dict[str, Any]
904
932
  if rel_schema and rel_schema.cardinality == "one":
905
- rel_data = RelatedNode._generate_query_data(peer_data=peer_data, property=property)
933
+ rel_data = RelatedNode._generate_query_data(
934
+ peer_data=peer_data, property=property, include_metadata=include_metadata
935
+ )
906
936
  # Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
907
937
  # tries to resolve attributes in this ancestor instead of actual node. To avoid
908
938
  # invalid queries issues when attribute is missing in the common ancestor, we use a fragment
@@ -912,7 +942,9 @@ class InfrahubNode(InfrahubNodeBase):
912
942
  rel_data["node"] = {}
913
943
  rel_data["node"][f"...on {rel_schema.peer}"] = data_node
914
944
  elif rel_schema and rel_schema.cardinality == "many":
915
- rel_data = RelationshipManager._generate_query_data(peer_data=peer_data, property=property)
945
+ rel_data = RelationshipManager._generate_query_data(
946
+ peer_data=peer_data, property=property, include_metadata=include_metadata
947
+ )
916
948
  else:
917
949
  continue
918
950
 
@@ -1285,8 +1317,12 @@ class InfrahubNodeSync(InfrahubNodeBase):
1285
1317
  """
1286
1318
  self._client = client
1287
1319
 
1288
- if isinstance(data, dict) and isinstance(data.get("node"), dict):
1289
- data = data.get("node")
1320
+ # Extract node_metadata before extracting node data (node_metadata is sibling to node in edges)
1321
+ node_metadata_data: dict | None = None
1322
+ if isinstance(data, dict):
1323
+ node_metadata_data = data.get("node_metadata")
1324
+ if isinstance(data.get("node"), dict):
1325
+ data = data.get("node")
1290
1326
 
1291
1327
  self._relationship_cardinality_many_data: dict[str, RelationshipManagerSync] = {}
1292
1328
  self._relationship_cardinality_one_data: dict[str, RelatedNodeSync] = {}
@@ -1294,6 +1330,10 @@ class InfrahubNodeSync(InfrahubNodeBase):
1294
1330
 
1295
1331
  super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
1296
1332
 
1333
+ # Initialize metadata after base class init
1334
+ if node_metadata_data:
1335
+ self._metadata = NodeMetadata(node_metadata_data)
1336
+
1297
1337
  @classmethod
1298
1338
  def from_graphql(
1299
1339
  cls,
@@ -1571,6 +1611,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1571
1611
  partial_match: bool = False,
1572
1612
  property: bool = False,
1573
1613
  order: Order | None = None,
1614
+ include_metadata: bool = False,
1574
1615
  ) -> dict[str, Any | dict]:
1575
1616
  data = self.generate_query_data_init(
1576
1617
  filters=filters,
@@ -1580,6 +1621,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1580
1621
  exclude=exclude,
1581
1622
  partial_match=partial_match,
1582
1623
  order=order,
1624
+ include_metadata=include_metadata,
1583
1625
  )
1584
1626
  data["edges"]["node"].update(
1585
1627
  self.generate_query_data_node(
@@ -1588,6 +1630,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1588
1630
  prefetch_relationships=prefetch_relationships,
1589
1631
  inherited=True,
1590
1632
  property=property,
1633
+ include_metadata=include_metadata,
1591
1634
  )
1592
1635
  )
1593
1636
 
@@ -1610,6 +1653,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1610
1653
  inherited=False,
1611
1654
  insert_alias=True,
1612
1655
  property=property,
1656
+ include_metadata=include_metadata,
1613
1657
  )
1614
1658
 
1615
1659
  if child_data:
@@ -1625,6 +1669,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1625
1669
  insert_alias: bool = False,
1626
1670
  prefetch_relationships: bool = False,
1627
1671
  property: bool = False,
1672
+ include_metadata: bool = False,
1628
1673
  ) -> dict[str, Any | dict]:
1629
1674
  """Generate the node part of a GraphQL Query with attributes and nodes.
1630
1675
 
@@ -1635,6 +1680,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1635
1680
  Defaults to True.
1636
1681
  insert_alias (bool, optional): If True, inserts aliases in the query for each attribute or relationship.
1637
1682
  prefetch_relationships (bool, optional): If True, pre-fetches relationship data as part of the query.
1683
+ include_metadata (bool, optional): If True, includes node_metadata and relationship_metadata in the query.
1638
1684
 
1639
1685
  Returns:
1640
1686
  dict[str, Union[Any, Dict]]: GraphQL query in dictionary format
@@ -1651,7 +1697,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1651
1697
  if not inherited and attr._schema.inherited:
1652
1698
  continue
1653
1699
 
1654
- attr_data = attr._generate_query_data(property=property)
1700
+ attr_data = attr._generate_query_data(property=property, include_metadata=include_metadata)
1655
1701
  if attr_data:
1656
1702
  data[attr_name] = attr_data
1657
1703
  if insert_alias:
@@ -1683,11 +1729,14 @@ class InfrahubNodeSync(InfrahubNodeBase):
1683
1729
  peer_node = InfrahubNodeSync(client=self._client, schema=peer_schema, branch=self._branch)
1684
1730
  peer_data = peer_node.generate_query_data_node(
1685
1731
  property=property,
1732
+ include_metadata=include_metadata,
1686
1733
  )
1687
1734
 
1688
1735
  rel_data: dict[str, Any]
1689
1736
  if rel_schema and rel_schema.cardinality == "one":
1690
- rel_data = RelatedNodeSync._generate_query_data(peer_data=peer_data, property=property)
1737
+ rel_data = RelatedNodeSync._generate_query_data(
1738
+ peer_data=peer_data, property=property, include_metadata=include_metadata
1739
+ )
1691
1740
  # Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
1692
1741
  # tries to resolve attributes in this ancestor instead of actual node. To avoid
1693
1742
  # invalid queries issues when attribute is missing in the common ancestor, we use a fragment
@@ -1697,7 +1746,9 @@ class InfrahubNodeSync(InfrahubNodeBase):
1697
1746
  rel_data["node"] = {}
1698
1747
  rel_data["node"][f"...on {rel_schema.peer}"] = data_node
1699
1748
  elif rel_schema and rel_schema.cardinality == "many":
1700
- rel_data = RelationshipManagerSync._generate_query_data(peer_data=peer_data, property=property)
1749
+ rel_data = RelationshipManagerSync._generate_query_data(
1750
+ peer_data=peer_data, property=property, include_metadata=include_metadata
1751
+ )
1701
1752
  else:
1702
1753
  continue
1703
1754
 
@@ -20,5 +20,8 @@ class NodeProperty:
20
20
  self.display_label = data.get("display_label", None)
21
21
  self.typename = data.get("__typename", None)
22
22
 
23
+ def __repr__(self) -> str:
24
+ return f"NodeProperty({{'id': {self.id!r}, 'display_label': {self.display_label!r}, '__typename': {self.typename!r}}})"
25
+
23
26
  def _generate_input_data(self) -> str | None:
24
27
  return self.id
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
6
6
  from ..exceptions import Error
7
7
  from ..protocols_base import CoreNodeBase
8
8
  from .constants import PROFILE_KIND_PREFIX, PROPERTIES_FLAG, PROPERTIES_OBJECT
9
+ from .metadata import NodeMetadata, RelationshipMetadata
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from ..client import InfrahubClient, InfrahubClientSync
@@ -40,11 +41,13 @@ class RelatedNodeBase:
40
41
  self._typename: str | None = None
41
42
  self._kind: str | None = None
42
43
  self._source_typename: str | None = None
44
+ self._relationship_metadata: RelationshipMetadata | None = None
43
45
 
44
46
  if isinstance(data, (CoreNodeBase)):
45
47
  self._peer = data
46
48
  for prop in self._properties:
47
49
  setattr(self, prop, None)
50
+ self._relationship_metadata = None
48
51
 
49
52
  elif isinstance(data, list):
50
53
  data = {"hfid": data}
@@ -81,6 +84,10 @@ class RelatedNodeBase:
81
84
  else:
82
85
  setattr(self, prop, None)
83
86
 
87
+ # Parse relationship metadata (at edge level)
88
+ if data.get("relationship_metadata"):
89
+ self._relationship_metadata = RelationshipMetadata(data["relationship_metadata"])
90
+
84
91
  @property
85
92
  def id(self) -> str | None:
86
93
  if self._peer:
@@ -134,6 +141,10 @@ class RelatedNodeBase:
134
141
  return False
135
142
  return bool(re.match(rf"^{PROFILE_KIND_PREFIX}[A-Z]", self._source_typename))
136
143
 
144
+ def get_relationship_metadata(self) -> RelationshipMetadata | None:
145
+ """Returns the relationship metadata (updated_at, updated_by) if fetched."""
146
+ return self._relationship_metadata
147
+
137
148
  def _generate_input_data(self, allocate_from_pool: bool = False) -> dict[str, Any]:
138
149
  data: dict[str, Any] = {}
139
150
 
@@ -160,12 +171,17 @@ class RelatedNodeBase:
160
171
  return {}
161
172
 
162
173
  @classmethod
163
- def _generate_query_data(cls, peer_data: dict[str, Any] | None = None, property: bool = False) -> dict:
174
+ def _generate_query_data(
175
+ cls, peer_data: dict[str, Any] | None = None, property: bool = False, include_metadata: bool = False
176
+ ) -> dict:
164
177
  """Generates the basic structure of a GraphQL query for a single relationship.
165
178
 
166
179
  Args:
167
180
  peer_data (dict[str, Union[Any, Dict]], optional): Additional data to be included in the query for the node.
168
181
  This is used to add extra fields when prefetching related node data.
182
+ property (bool, optional): If True, includes property fields (is_protected, source, owner, etc.).
183
+ include_metadata (bool, optional): If True, includes node_metadata (for the peer node) and
184
+ relationship_metadata (for the relationship edge) fields.
169
185
 
170
186
  Returns:
171
187
  Dict: A dictionary representing the basic structure of a GraphQL query, including the node's ID, display label,
@@ -181,6 +197,13 @@ class RelatedNodeBase:
181
197
  properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
182
198
 
183
199
  data["properties"] = properties
200
+
201
+ if include_metadata:
202
+ # node_metadata is for the peer InfrahubNode (populated via from_graphql)
203
+ data["node_metadata"] = NodeMetadata._generate_query_data()
204
+ # relationship_metadata is for the relationship edge itself
205
+ data["relationship_metadata"] = RelationshipMetadata._generate_query_data()
206
+
184
207
  if peer_data:
185
208
  data["node"].update(peer_data)
186
209
 
@@ -10,6 +10,7 @@ from ..exceptions import (
10
10
  )
11
11
  from ..types import Order
12
12
  from .constants import PROPERTIES_FLAG, PROPERTIES_OBJECT
13
+ from .metadata import NodeMetadata, RelationshipMetadata
13
14
  from .related_node import RelatedNode, RelatedNodeSync
14
15
 
15
16
  if TYPE_CHECKING:
@@ -72,12 +73,16 @@ class RelationshipManagerBase:
72
73
  return {}
73
74
 
74
75
  @classmethod
75
- def _generate_query_data(cls, peer_data: dict[str, Any] | None = None, property: bool = False) -> dict:
76
+ def _generate_query_data(
77
+ cls, peer_data: dict[str, Any] | None = None, property: bool = False, include_metadata: bool = False
78
+ ) -> dict:
76
79
  """Generates the basic structure of a GraphQL query for relationships with multiple nodes.
77
80
 
78
81
  Args:
79
82
  peer_data (dict[str, Union[Any, Dict]], optional): Additional data to be included in the query for each node.
80
83
  This is used to add extra fields when prefetching related node data in many-to-many relationships.
84
+ property (bool, optional): If True, includes property fields (is_protected, source, owner, etc.).
85
+ include_metadata (bool, optional): If True, includes node_metadata and relationship_metadata fields.
81
86
 
82
87
  Returns:
83
88
  Dict: A dictionary representing the basic structure of a GraphQL query for multiple related nodes.
@@ -97,6 +102,10 @@ class RelationshipManagerBase:
97
102
  properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
98
103
  data["edges"]["properties"] = properties
99
104
 
105
+ if include_metadata:
106
+ data["edges"]["node_metadata"] = NodeMetadata._generate_query_data()
107
+ data["edges"]["relationship_metadata"] = RelationshipMetadata._generate_query_data()
108
+
100
109
  if peer_data:
101
110
  data["edges"]["node"].update(peer_data)
102
111
 
infrahub_sdk/operation.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
3
+ import pathlib
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  from .repository import GitRepoManager
@@ -22,7 +22,7 @@ class InfrahubOperation:
22
22
  ) -> None:
23
23
  self.branch = branch
24
24
  self.convert_query_response = convert_query_response
25
- self.root_directory = root_directory or os.getcwd()
25
+ self.root_directory = root_directory or str(pathlib.Path.cwd())
26
26
  self.infrahub_node = infrahub_node
27
27
  self._nodes: list[InfrahubNode] = []
28
28
  self._related_nodes: list[InfrahubNode] = []
@@ -151,8 +151,7 @@ class InfrahubRepositoryGraphQLConfig(InfrahubRepositoryConfigElement):
151
151
 
152
152
  def load_query(self, relative_path: str = ".") -> str:
153
153
  file_name = Path(f"{relative_path}/{self.file_path}")
154
- with file_name.open("r", encoding="UTF-8") as file:
155
- return file.read()
154
+ return file_name.read_text(encoding="UTF-8")
156
155
 
157
156
 
158
157
  class InfrahubObjectConfig(InfrahubRepositoryConfigElement):
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
3
+ import inspect
4
4
  import os
5
5
  from abc import abstractmethod
6
6
  from typing import TYPE_CHECKING, Any
@@ -75,7 +75,7 @@ class InfrahubTransform(InfrahubOperation):
75
75
  unpacked = data.get("data") or data
76
76
  await self.process_nodes(data=unpacked)
77
77
 
78
- if asyncio.iscoroutinefunction(self.transform):
78
+ if inspect.iscoroutinefunction(self.transform):
79
79
  return await self.transform(data=unpacked)
80
80
 
81
81
  return self.transform(data=unpacked)
infrahub_sdk/types.py CHANGED
@@ -4,7 +4,9 @@ import enum
4
4
  from logging import Logger
5
5
  from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+ from infrahub_sdk.enums import OrderDirection # noqa: TC001
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  import httpx
@@ -68,5 +70,19 @@ class InfrahubLogger(Protocol):
68
70
  InfrahubLoggers = InfrahubLogger | Logger
69
71
 
70
72
 
73
+ class NodeMetaOrder(BaseModel):
74
+ created_at: OrderDirection | None = None
75
+ updated_at: OrderDirection | None = None
76
+
77
+ @model_validator(mode="after")
78
+ def validate_selection(self) -> NodeMetaOrder:
79
+ if self.created_at and self.updated_at:
80
+ raise ValueError("'created_at' and 'updated_at' are mutually exclusive")
81
+ return self
82
+
83
+
71
84
  class Order(BaseModel):
72
- disable: bool | None = None
85
+ disable: bool | None = Field(
86
+ default=None, description="Disable default ordering, can be used to improve performance"
87
+ )
88
+ node_metadata: NodeMetaOrder | None = Field(default=None, description="Order by node meta fields")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: infrahub-server
3
- Version: 1.7.0rc0
3
+ Version: 1.7.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
@@ -17,7 +17,7 @@ Requires-Dist: aio-pika<9.5,>=9.4
17
17
  Requires-Dist: aiodataloader==0.4.3
18
18
  Requires-Dist: ariadne-codegen==0.15.3
19
19
  Requires-Dist: asgi-correlation-id==4.2.0
20
- Requires-Dist: authlib==1.6.5
20
+ Requires-Dist: authlib==1.6.6
21
21
  Requires-Dist: bcrypt<4.2,>=4.1
22
22
  Requires-Dist: boto3==1.34.129
23
23
  Requires-Dist: cachetools-async==0.0.5
@@ -44,13 +44,13 @@ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.39.0
44
44
  Requires-Dist: opentelemetry-instrumentation-aio-pika==0.60b0
45
45
  Requires-Dist: opentelemetry-instrumentation-fastapi==0.60b0
46
46
  Requires-Dist: prefect-redis==0.2.8
47
- Requires-Dist: prefect==3.6.7
47
+ Requires-Dist: prefect==3.6.13
48
48
  Requires-Dist: pyarrow>=14
49
49
  Requires-Dist: pydantic-settings<2.9,>=2.8
50
50
  Requires-Dist: pydantic<2.13,>=2.12
51
51
  Requires-Dist: pyjwt<2.9,>=2.8
52
52
  Requires-Dist: pytest<9.1,>=9.0
53
- Requires-Dist: python-multipart==0.0.18
53
+ Requires-Dist: python-multipart==0.0.22
54
54
  Requires-Dist: pyyaml<7,>=6
55
55
  Requires-Dist: redis[hiredis]==6.0.0
56
56
  Requires-Dist: rich<14,>=13
@@ -64,15 +64,15 @@ Requires-Dist: whenever==0.9.3
64
64
  Description-Content-Type: text/markdown
65
65
 
66
66
  <h1 align="center">
67
- <a href=""><img src="docs/static/img/infrahub-hori.svg" alt="Infrahub" width="350"></a>
67
+ <a href="https://docs.infrahub.app"><img src="docs/static/img/infrahub-hori.svg" alt="Infrahub logo" width="350"></a>
68
68
  </h1>
69
69
 
70
70
  <p align="center">
71
71
  <a href="https://www.linkedin.com/company/opsmill">
72
- <img src="https://img.shields.io/badge/linkedin-blue?logo=LinkedIn"/>
72
+ <img src="https://img.shields.io/badge/linkedin-blue?logo=LinkedIn" alt="LinkedIn badge"/>
73
73
  </a>
74
74
  <a href="https://discord.gg/opsmill">
75
- <img src="https://img.shields.io/badge/Discord-7289DA?&logo=discord&logoColor=white"/>
75
+ <img src="https://img.shields.io/badge/Discord-7289DA?&logo=discord&logoColor=white" alt="Discord badge"/>
76
76
  </a>
77
77
  </p>
78
78
 
@@ -131,7 +131,7 @@ If you need help, support for the community version of Infrahub is provided on [
131
131
  To help our community with the creation of contributions, please view our [CONTRIBUTING](./CONTRIBUTING.md) page.
132
132
 
133
133
  <a href="https://github.com/opsmill/infrahub/graphs/contributors">
134
- <img src="https://contrib.rocks/image?repo=opsmill/infrahub" />
134
+ <img src="https://contrib.rocks/image?repo=opsmill/infrahub" alt="Contributors"/>
135
135
  </a>
136
136
 
137
137
  ## Security