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.
- infrahub/actions/gather.py +2 -2
- infrahub/api/query.py +3 -2
- infrahub/api/schema.py +5 -0
- infrahub/api/transformation.py +3 -3
- infrahub/cli/db.py +6 -2
- infrahub/computed_attribute/gather.py +2 -0
- infrahub/config.py +2 -2
- infrahub/core/attribute.py +21 -2
- infrahub/core/branch/models.py +11 -117
- infrahub/core/branch/tasks.py +7 -3
- infrahub/core/diff/merger/merger.py +5 -1
- infrahub/core/diff/model/path.py +43 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/index.py +2 -0
- infrahub/core/initialization.py +2 -1
- infrahub/core/ipam/resource_allocator.py +229 -0
- infrahub/core/migrations/graph/__init__.py +10 -0
- infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
- infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
- infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
- infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
- infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
- infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
- infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
- infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
- infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
- infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +3 -2
- infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
- infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
- infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
- infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
- infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +3 -3
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
- infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
- infrahub/core/migrations/graph/m052_fix_global_branch_level.py +51 -0
- infrahub/core/migrations/graph/m053_fix_branch_level_zero.py +61 -0
- infrahub/core/migrations/graph/m054_cleanup_orphaned_nodes.py +87 -0
- infrahub/core/migrations/graph/m055_remove_webhook_validate_certificates_default.py +86 -0
- infrahub/core/migrations/runner.py +6 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
- infrahub/core/migrations/schema/attribute_supports_profile.py +3 -8
- infrahub/core/migrations/schema/models.py +8 -0
- infrahub/core/migrations/schema/node_attribute_add.py +24 -29
- infrahub/core/migrations/schema/tasks.py +7 -1
- infrahub/core/migrations/shared.py +37 -30
- infrahub/core/node/__init__.py +2 -1
- infrahub/core/node/lock_utils.py +23 -2
- infrahub/core/node/resource_manager/ip_address_pool.py +5 -11
- infrahub/core/node/resource_manager/ip_prefix_pool.py +5 -21
- infrahub/core/node/resource_manager/number_pool.py +109 -39
- infrahub/core/query/__init__.py +7 -1
- infrahub/core/query/branch.py +18 -2
- infrahub/core/query/ipam.py +629 -40
- infrahub/core/query/node.py +128 -0
- infrahub/core/query/resource_manager.py +114 -1
- infrahub/core/relationship/model.py +9 -3
- infrahub/core/schema/attribute_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +9 -2
- infrahub/core/schema/definitions/core/webhook.py +0 -1
- infrahub/core/schema/definitions/internal.py +7 -4
- infrahub/core/schema/manager.py +50 -38
- infrahub/core/validators/attribute/kind.py +5 -2
- infrahub/core/validators/determiner.py +4 -0
- infrahub/graphql/analyzer.py +3 -1
- infrahub/graphql/app.py +7 -10
- infrahub/graphql/execution.py +95 -0
- infrahub/graphql/manager.py +8 -2
- infrahub/graphql/mutations/proposed_change.py +15 -0
- infrahub/graphql/parser.py +10 -7
- infrahub/graphql/queries/ipam.py +20 -25
- infrahub/graphql/queries/search.py +29 -9
- infrahub/lock.py +7 -0
- infrahub/proposed_change/tasks.py +2 -0
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/services/adapters/http/httpx.py +27 -0
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +73 -4
- infrahub/trigger/setup.py +1 -1
- infrahub/trigger/system.py +36 -0
- infrahub/webhook/models.py +4 -2
- infrahub/webhook/tasks.py +2 -2
- infrahub/workflows/initialization.py +2 -2
- infrahub_sdk/analyzer.py +2 -2
- infrahub_sdk/branch.py +12 -39
- infrahub_sdk/checks.py +4 -4
- infrahub_sdk/client.py +36 -0
- infrahub_sdk/ctl/cli_commands.py +2 -1
- infrahub_sdk/ctl/graphql.py +15 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/enums.py +6 -0
- infrahub_sdk/graphql/renderers.py +21 -0
- infrahub_sdk/graphql/utils.py +85 -0
- infrahub_sdk/node/attribute.py +12 -2
- infrahub_sdk/node/constants.py +11 -0
- infrahub_sdk/node/metadata.py +69 -0
- infrahub_sdk/node/node.py +65 -14
- infrahub_sdk/node/property.py +3 -0
- infrahub_sdk/node/related_node.py +24 -1
- infrahub_sdk/node/relationship.py +10 -1
- infrahub_sdk/operation.py +2 -2
- infrahub_sdk/schema/repository.py +1 -2
- infrahub_sdk/transforms.py +2 -2
- infrahub_sdk/types.py +18 -2
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/METADATA +8 -8
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/RECORD +123 -114
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/entry_points.txt +0 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +16 -10
- infrahub_testcontainers/docker-compose.test.yml +11 -10
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/pools/address.py +0 -16
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/WHEEL +0 -0
- {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
|
|
196
|
-
return [self._schema.kind] +
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1289
|
-
|
|
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(
|
|
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(
|
|
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
|
|
infrahub_sdk/node/property.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
155
|
-
return file.read()
|
|
154
|
+
return file_name.read_text(encoding="UTF-8")
|
|
156
155
|
|
|
157
156
|
|
|
158
157
|
class InfrahubObjectConfig(InfrahubRepositoryConfigElement):
|
infrahub_sdk/transforms.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
134
|
+
<img src="https://contrib.rocks/image?repo=opsmill/infrahub" alt="Contributors"/>
|
|
135
135
|
</a>
|
|
136
136
|
|
|
137
137
|
## Security
|