infrahub-server 1.6.2__py3-none-any.whl → 1.7.0__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/tasks.py +4 -2
- infrahub/api/exceptions.py +2 -2
- infrahub/api/schema.py +3 -1
- infrahub/artifacts/tasks.py +1 -0
- infrahub/auth.py +2 -2
- infrahub/cli/db.py +54 -28
- infrahub/computed_attribute/gather.py +3 -4
- infrahub/computed_attribute/tasks.py +23 -6
- infrahub/config.py +8 -0
- infrahub/constants/enums.py +12 -0
- infrahub/core/account.py +12 -9
- infrahub/core/attribute.py +106 -108
- infrahub/core/branch/models.py +44 -71
- infrahub/core/branch/tasks.py +5 -3
- infrahub/core/changelog/diff.py +1 -20
- infrahub/core/changelog/models.py +0 -7
- infrahub/core/constants/__init__.py +17 -0
- infrahub/core/constants/database.py +0 -1
- infrahub/core/constants/schema.py +0 -1
- infrahub/core/convert_object_type/repository_conversion.py +3 -4
- infrahub/core/diff/branch_differ.py +1 -1
- infrahub/core/diff/conflict_transferer.py +1 -1
- infrahub/core/diff/data_check_synchronizer.py +4 -3
- infrahub/core/diff/enricher/cardinality_one.py +2 -2
- infrahub/core/diff/enricher/hierarchy.py +1 -1
- infrahub/core/diff/enricher/labels.py +1 -1
- infrahub/core/diff/merger/merger.py +28 -2
- infrahub/core/diff/merger/serializer.py +3 -10
- infrahub/core/diff/model/diff.py +1 -1
- infrahub/core/diff/query/merge.py +376 -135
- infrahub/core/diff/repository/repository.py +3 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +3 -3
- infrahub/core/graph/schema.py +2 -12
- infrahub/core/ipam/reconciler.py +8 -6
- infrahub/core/ipam/utilization.py +8 -15
- infrahub/core/manager.py +133 -152
- infrahub/core/merge.py +1 -1
- infrahub/core/metadata/__init__.py +0 -0
- infrahub/core/metadata/interface.py +37 -0
- infrahub/core/metadata/model.py +31 -0
- infrahub/core/metadata/query/__init__.py +0 -0
- infrahub/core/metadata/query/node_metadata.py +301 -0
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -12
- infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
- infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
- infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +1 -1
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +53 -0
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
- infrahub/core/migrations/query/__init__.py +2 -2
- infrahub/core/migrations/query/attribute_add.py +17 -6
- infrahub/core/migrations/query/attribute_remove.py +19 -5
- infrahub/core/migrations/query/attribute_rename.py +21 -5
- infrahub/core/migrations/query/node_duplicate.py +19 -4
- infrahub/core/migrations/query/schema_attribute_update.py +1 -1
- infrahub/core/migrations/schema/attribute_kind_update.py +26 -6
- infrahub/core/migrations/schema/attribute_name_update.py +1 -1
- infrahub/core/migrations/schema/attribute_supports_profile.py +5 -3
- infrahub/core/migrations/schema/models.py +3 -0
- infrahub/core/migrations/schema/node_attribute_add.py +5 -2
- infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
- infrahub/core/migrations/schema/node_kind_update.py +1 -1
- infrahub/core/migrations/schema/node_remove.py +24 -2
- infrahub/core/migrations/schema/tasks.py +4 -1
- infrahub/core/migrations/shared.py +13 -6
- infrahub/core/models.py +6 -6
- infrahub/core/node/__init__.py +157 -58
- infrahub/core/node/base.py +9 -5
- infrahub/core/node/create.py +7 -3
- infrahub/core/node/delete_validator.py +1 -1
- infrahub/core/node/standard.py +100 -14
- infrahub/core/order.py +30 -0
- infrahub/core/property.py +0 -1
- infrahub/core/protocols.py +1 -0
- infrahub/core/protocols_base.py +10 -2
- infrahub/core/query/__init__.py +11 -6
- infrahub/core/query/attribute.py +164 -49
- infrahub/core/query/branch.py +58 -70
- infrahub/core/query/delete.py +1 -1
- infrahub/core/query/diff.py +7 -7
- infrahub/core/query/ipam.py +104 -43
- infrahub/core/query/node.py +1072 -281
- infrahub/core/query/relationship.py +531 -325
- infrahub/core/query/resource_manager.py +107 -18
- infrahub/core/query/standard_node.py +25 -5
- infrahub/core/query/utils.py +2 -4
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/constraints/peer_kind.py +1 -1
- infrahub/core/relationship/constraints/peer_parent.py +1 -1
- infrahub/core/relationship/constraints/peer_relatives.py +1 -1
- infrahub/core/relationship/constraints/profiles_kind.py +1 -1
- infrahub/core/relationship/constraints/profiles_removal.py +168 -0
- infrahub/core/relationship/model.py +293 -139
- infrahub/core/schema/attribute_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +11 -17
- infrahub/core/schema/basenode_schema.py +3 -0
- infrahub/core/schema/definitions/core/__init__.py +8 -2
- infrahub/core/schema/definitions/core/account.py +10 -10
- infrahub/core/schema/definitions/core/artifact.py +14 -8
- infrahub/core/schema/definitions/core/check.py +10 -4
- infrahub/core/schema/definitions/core/generator.py +26 -6
- infrahub/core/schema/definitions/core/graphql_query.py +1 -1
- infrahub/core/schema/definitions/core/group.py +9 -2
- infrahub/core/schema/definitions/core/ipam.py +80 -10
- infrahub/core/schema/definitions/core/menu.py +41 -7
- infrahub/core/schema/definitions/core/permission.py +16 -2
- infrahub/core/schema/definitions/core/profile.py +16 -2
- infrahub/core/schema/definitions/core/propose_change.py +24 -4
- infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
- infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
- infrahub/core/schema/definitions/core/repository.py +10 -0
- infrahub/core/schema/definitions/core/resource_pool.py +8 -1
- infrahub/core/schema/definitions/core/template.py +19 -2
- infrahub/core/schema/definitions/core/transform.py +11 -5
- infrahub/core/schema/definitions/core/webhook.py +27 -9
- infrahub/core/schema/manager.py +63 -43
- infrahub/core/schema/relationship_schema.py +6 -2
- infrahub/core/schema/schema_branch.py +115 -11
- infrahub/core/task/task.py +4 -2
- infrahub/core/utils.py +3 -25
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/attribute/choices.py +1 -1
- infrahub/core/validators/attribute/enum.py +1 -1
- infrahub/core/validators/attribute/kind.py +6 -3
- infrahub/core/validators/attribute/length.py +1 -1
- infrahub/core/validators/attribute/min_max.py +1 -1
- infrahub/core/validators/attribute/number_pool.py +1 -1
- infrahub/core/validators/attribute/optional.py +1 -1
- infrahub/core/validators/attribute/regex.py +1 -1
- infrahub/core/validators/determiner.py +3 -3
- infrahub/core/validators/node/attribute.py +1 -1
- infrahub/core/validators/node/relationship.py +1 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/database/__init__.py +4 -4
- infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
- infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
- infrahub/dependencies/registry.py +2 -0
- infrahub/display_labels/tasks.py +12 -3
- infrahub/git/integrator.py +18 -18
- infrahub/git/tasks.py +1 -1
- infrahub/git/utils.py +1 -1
- infrahub/graphql/app.py +2 -2
- infrahub/graphql/constants.py +3 -0
- infrahub/graphql/context.py +1 -1
- infrahub/graphql/field_extractor.py +1 -1
- infrahub/graphql/initialization.py +11 -0
- infrahub/graphql/loaders/account.py +134 -0
- infrahub/graphql/loaders/node.py +5 -12
- infrahub/graphql/loaders/peers.py +5 -7
- infrahub/graphql/manager.py +175 -21
- infrahub/graphql/metadata.py +91 -0
- infrahub/graphql/mutations/account.py +6 -6
- infrahub/graphql/mutations/attribute.py +0 -2
- infrahub/graphql/mutations/branch.py +9 -5
- infrahub/graphql/mutations/computed_attribute.py +1 -1
- infrahub/graphql/mutations/display_label.py +1 -1
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/ipam.py +4 -6
- infrahub/graphql/mutations/main.py +9 -4
- infrahub/graphql/mutations/profile.py +16 -22
- infrahub/graphql/mutations/proposed_change.py +4 -4
- infrahub/graphql/mutations/relationship.py +40 -10
- infrahub/graphql/mutations/repository.py +14 -12
- infrahub/graphql/mutations/schema.py +2 -2
- infrahub/graphql/order.py +14 -0
- infrahub/graphql/queries/branch.py +62 -6
- infrahub/graphql/queries/diff/tree.py +5 -5
- infrahub/graphql/queries/resource_manager.py +25 -24
- infrahub/graphql/resolvers/account_metadata.py +84 -0
- infrahub/graphql/resolvers/ipam.py +6 -8
- infrahub/graphql/resolvers/many_relationship.py +77 -35
- infrahub/graphql/resolvers/resolver.py +59 -14
- infrahub/graphql/resolvers/single_relationship.py +87 -23
- infrahub/graphql/subscription/graphql_query.py +2 -0
- infrahub/graphql/types/__init__.py +0 -1
- infrahub/graphql/types/attribute.py +10 -5
- infrahub/graphql/types/branch.py +40 -53
- infrahub/graphql/types/enums.py +3 -0
- infrahub/graphql/types/metadata.py +28 -0
- infrahub/graphql/types/node.py +22 -2
- infrahub/graphql/types/relationship.py +10 -2
- infrahub/graphql/types/standard_node.py +12 -7
- infrahub/hfid/tasks.py +12 -3
- infrahub/lock.py +7 -0
- infrahub/menu/repository.py +1 -1
- infrahub/patch/queries/base.py +1 -1
- infrahub/pools/number.py +1 -8
- infrahub/profiles/gather.py +56 -0
- infrahub/profiles/mandatory_fields_checker.py +116 -0
- infrahub/profiles/models.py +66 -0
- infrahub/profiles/node_applier.py +154 -13
- infrahub/profiles/queries/get_profile_data.py +143 -31
- infrahub/profiles/tasks.py +79 -27
- infrahub/profiles/triggers.py +22 -0
- infrahub/proposed_change/action_checker.py +1 -1
- infrahub/proposed_change/tasks.py +4 -1
- infrahub/services/__init__.py +1 -1
- infrahub/services/adapters/cache/nats.py +1 -1
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/tasks/artifact.py +1 -0
- infrahub/transformations/tasks.py +2 -2
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +1 -0
- infrahub/trigger/setup.py +3 -3
- infrahub/trigger/tasks.py +3 -0
- infrahub/validators/tasks.py +1 -0
- infrahub/webhook/gather.py +1 -1
- infrahub/webhook/models.py +1 -1
- infrahub/webhook/tasks.py +23 -7
- infrahub/workers/dependencies.py +9 -3
- infrahub/workers/infrahub_async.py +13 -4
- infrahub/workflows/catalogue.py +19 -0
- 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 +12 -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 +37 -5
- infrahub_sdk/node/relationship.py +18 -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.6.2.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
- {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +252 -231
- infrahub_testcontainers/container.py +3 -3
- infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
- infrahub_testcontainers/docker-compose.test.yml +13 -5
- infrahub_testcontainers/models.py +3 -3
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/graphql/models.py +0 -6
- {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
infrahub_sdk/enums.py
ADDED
|
@@ -7,6 +7,8 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from infrahub_sdk.types import Order
|
|
11
|
+
|
|
10
12
|
from .constants import VARIABLE_TYPE_MAPPING
|
|
11
13
|
|
|
12
14
|
|
|
@@ -53,6 +55,16 @@ def convert_to_graphql_as_string(value: Any, convert_enum: bool = False) -> str:
|
|
|
53
55
|
if isinstance(value, list):
|
|
54
56
|
values_as_string = [convert_to_graphql_as_string(value=item, convert_enum=convert_enum) for item in value]
|
|
55
57
|
return "[" + ", ".join(values_as_string) + "]"
|
|
58
|
+
if isinstance(value, Order):
|
|
59
|
+
data = value.model_dump(exclude_none=True)
|
|
60
|
+
return (
|
|
61
|
+
"{ "
|
|
62
|
+
+ ", ".join(
|
|
63
|
+
f"{key}: {convert_to_graphql_as_string(value=val, convert_enum=convert_enum)}"
|
|
64
|
+
for key, val in data.items()
|
|
65
|
+
)
|
|
66
|
+
+ " }"
|
|
67
|
+
)
|
|
56
68
|
if isinstance(value, BaseModel):
|
|
57
69
|
data = value.model_dump()
|
|
58
70
|
return (
|
|
@@ -63,6 +75,15 @@ def convert_to_graphql_as_string(value: Any, convert_enum: bool = False) -> str:
|
|
|
63
75
|
)
|
|
64
76
|
+ " }"
|
|
65
77
|
)
|
|
78
|
+
if isinstance(value, dict):
|
|
79
|
+
return (
|
|
80
|
+
"{ "
|
|
81
|
+
+ ", ".join(
|
|
82
|
+
f"{key}: {convert_to_graphql_as_string(value=val, convert_enum=convert_enum)}"
|
|
83
|
+
for key, val in value.items()
|
|
84
|
+
)
|
|
85
|
+
+ " }"
|
|
86
|
+
)
|
|
66
87
|
|
|
67
88
|
return str(value)
|
|
68
89
|
|
infrahub_sdk/graphql/utils.py
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
|
|
3
|
+
from graphql import (
|
|
4
|
+
FieldNode,
|
|
5
|
+
FragmentDefinitionNode,
|
|
6
|
+
FragmentSpreadNode,
|
|
7
|
+
InlineFragmentNode,
|
|
8
|
+
OperationDefinitionNode,
|
|
9
|
+
SelectionNode,
|
|
10
|
+
SelectionSetNode,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def strip_typename_from_selection_set(selection_set: SelectionSetNode | None) -> SelectionSetNode | None:
|
|
15
|
+
"""Recursively strip __typename fields from a SelectionSetNode.
|
|
16
|
+
|
|
17
|
+
The __typename meta-field is an introspection field that is not part of the schema's
|
|
18
|
+
type definitions. When code generation tools like ariadne-codegen try to look up
|
|
19
|
+
__typename in the schema, they fail because it's a reserved introspection field.
|
|
20
|
+
|
|
21
|
+
This function removes all __typename fields from the selection set, allowing
|
|
22
|
+
code generation to proceed without errors.
|
|
23
|
+
"""
|
|
24
|
+
if selection_set is None:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
new_selections: list[SelectionNode] = []
|
|
28
|
+
for selection in selection_set.selections:
|
|
29
|
+
if isinstance(selection, FieldNode):
|
|
30
|
+
# Skip __typename fields
|
|
31
|
+
if selection.name.value == "__typename":
|
|
32
|
+
continue
|
|
33
|
+
# Recursively process nested selection sets
|
|
34
|
+
new_field = FieldNode(
|
|
35
|
+
alias=selection.alias,
|
|
36
|
+
name=selection.name,
|
|
37
|
+
arguments=selection.arguments,
|
|
38
|
+
directives=selection.directives,
|
|
39
|
+
selection_set=strip_typename_from_selection_set(selection.selection_set),
|
|
40
|
+
)
|
|
41
|
+
new_selections.append(new_field)
|
|
42
|
+
elif isinstance(selection, InlineFragmentNode):
|
|
43
|
+
# Process inline fragments
|
|
44
|
+
new_inline = InlineFragmentNode(
|
|
45
|
+
type_condition=selection.type_condition,
|
|
46
|
+
directives=selection.directives,
|
|
47
|
+
selection_set=strip_typename_from_selection_set(selection.selection_set),
|
|
48
|
+
)
|
|
49
|
+
new_selections.append(new_inline)
|
|
50
|
+
elif isinstance(selection, FragmentSpreadNode):
|
|
51
|
+
# FragmentSpread references a named fragment - keep as-is
|
|
52
|
+
new_selections.append(selection)
|
|
53
|
+
else:
|
|
54
|
+
raise TypeError(f"Unexpected GraphQL selection node type '{type(selection).__name__}'.")
|
|
55
|
+
|
|
56
|
+
return SelectionSetNode(selections=tuple(new_selections))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def strip_typename_from_operation(operation: OperationDefinitionNode) -> OperationDefinitionNode:
|
|
60
|
+
"""Strip __typename fields from an operation definition.
|
|
61
|
+
|
|
62
|
+
Returns a new OperationDefinitionNode with all __typename fields removed
|
|
63
|
+
from its selection set and any nested selection sets.
|
|
64
|
+
"""
|
|
65
|
+
return OperationDefinitionNode(
|
|
66
|
+
operation=operation.operation,
|
|
67
|
+
name=operation.name,
|
|
68
|
+
variable_definitions=operation.variable_definitions,
|
|
69
|
+
directives=operation.directives,
|
|
70
|
+
selection_set=strip_typename_from_selection_set(operation.selection_set),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def strip_typename_from_fragment(fragment: FragmentDefinitionNode) -> FragmentDefinitionNode:
|
|
75
|
+
"""Strip __typename fields from a fragment definition.
|
|
76
|
+
|
|
77
|
+
Returns a new FragmentDefinitionNode with all __typename fields removed
|
|
78
|
+
from its selection set and any nested selection sets.
|
|
79
|
+
"""
|
|
80
|
+
return FragmentDefinitionNode(
|
|
81
|
+
name=fragment.name,
|
|
82
|
+
type_condition=fragment.type_condition,
|
|
83
|
+
variable_definitions=fragment.variable_definitions,
|
|
84
|
+
directives=fragment.directives,
|
|
85
|
+
selection_set=strip_typename_from_selection_set(fragment.selection_set),
|
|
86
|
+
)
|
|
87
|
+
|
|
3
88
|
|
|
4
89
|
def get_class_def_index(module: ast.Module) -> int:
|
|
5
90
|
"""Get the index of the first class definition in the module.
|
infrahub_sdk/node/attribute.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, get_args
|
|
|
6
6
|
|
|
7
7
|
from ..protocols_base import CoreNodeBase
|
|
8
8
|
from ..uuidt import UUIDT
|
|
9
|
-
from .constants import IP_TYPES, PROPERTIES_FLAG, PROPERTIES_OBJECT, SAFE_VALUE
|
|
9
|
+
from .constants import ATTRIBUTE_METADATA_OBJECT, IP_TYPES, PROPERTIES_FLAG, PROPERTIES_OBJECT, SAFE_VALUE
|
|
10
10
|
from .property import NodeProperty
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -57,11 +57,16 @@ class Attribute:
|
|
|
57
57
|
|
|
58
58
|
self.source: NodeProperty | None = None
|
|
59
59
|
self.owner: NodeProperty | None = None
|
|
60
|
+
self.updated_by: NodeProperty | None = None
|
|
60
61
|
|
|
61
62
|
for prop_name in self._properties_object:
|
|
62
63
|
if data.get(prop_name):
|
|
63
64
|
setattr(self, prop_name, NodeProperty(data=data.get(prop_name))) # type: ignore[arg-type]
|
|
64
65
|
|
|
66
|
+
for prop_name in ATTRIBUTE_METADATA_OBJECT:
|
|
67
|
+
if data.get(prop_name):
|
|
68
|
+
setattr(self, prop_name, NodeProperty(data=data.get(prop_name))) # type: ignore[arg-type]
|
|
69
|
+
|
|
65
70
|
@property
|
|
66
71
|
def value(self) -> Any:
|
|
67
72
|
return self._value
|
|
@@ -104,7 +109,7 @@ class Attribute:
|
|
|
104
109
|
|
|
105
110
|
return {"data": data, "variables": variables}
|
|
106
111
|
|
|
107
|
-
def _generate_query_data(self, property: bool = False) -> dict | None:
|
|
112
|
+
def _generate_query_data(self, property: bool = False, include_metadata: bool = False) -> dict | None:
|
|
108
113
|
data: dict[str, Any] = {"value": None}
|
|
109
114
|
|
|
110
115
|
if property:
|
|
@@ -115,6 +120,11 @@ class Attribute:
|
|
|
115
120
|
for prop_name in self._properties_object:
|
|
116
121
|
data[prop_name] = {"id": None, "display_label": None, "__typename": None}
|
|
117
122
|
|
|
123
|
+
if include_metadata:
|
|
124
|
+
data["updated_at"] = None
|
|
125
|
+
for prop_name in ATTRIBUTE_METADATA_OBJECT:
|
|
126
|
+
data[prop_name] = {"id": None, "display_label": None, "__typename": None}
|
|
127
|
+
|
|
118
128
|
return data
|
|
119
129
|
|
|
120
130
|
def _generate_mutation_query(self) -> dict[str, Any]:
|
infrahub_sdk/node/constants.py
CHANGED
|
@@ -3,6 +3,17 @@ import re
|
|
|
3
3
|
|
|
4
4
|
PROPERTIES_FLAG = ["is_protected", "updated_at"]
|
|
5
5
|
PROPERTIES_OBJECT = ["source", "owner"]
|
|
6
|
+
|
|
7
|
+
# Attribute-level metadata object fields (in addition to PROPERTIES_OBJECT)
|
|
8
|
+
ATTRIBUTE_METADATA_OBJECT = ["updated_by"]
|
|
9
|
+
|
|
10
|
+
# Node metadata fields (for node_metadata in GraphQL response)
|
|
11
|
+
NODE_METADATA_FIELDS_FLAG = ["created_at", "updated_at"]
|
|
12
|
+
NODE_METADATA_FIELDS_OBJECT = ["created_by", "updated_by"]
|
|
13
|
+
|
|
14
|
+
# Relationship metadata fields (for relationship_metadata in GraphQL response)
|
|
15
|
+
RELATIONSHIP_METADATA_FIELDS_FLAG = ["updated_at"]
|
|
16
|
+
RELATIONSHIP_METADATA_FIELDS_OBJECT = ["updated_by"]
|
|
6
17
|
SAFE_VALUE = re.compile(r"(^[\. /:a-zA-Z0-9_-]+$)|(^$)")
|
|
7
18
|
|
|
8
19
|
IP_TYPES = ipaddress.IPv4Interface | ipaddress.IPv6Interface | ipaddress.IPv4Network | ipaddress.IPv6Network
|
|
@@ -20,3 +31,4 @@ ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
|
|
|
20
31
|
HIERARCHY_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE = "Hierarchical fields are not supported for this node."
|
|
21
32
|
|
|
22
33
|
HFID_STR_SEPARATOR = "__"
|
|
34
|
+
PROFILE_KIND_PREFIX = "Profile"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .property import NodeProperty
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NodeMetadata:
|
|
7
|
+
"""Represents metadata about a node (created_at, created_by, updated_at, updated_by)."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, data: dict | None = None) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Args:
|
|
12
|
+
data: Data containing the metadata fields from the GraphQL response.
|
|
13
|
+
"""
|
|
14
|
+
self.created_at: str | None = None
|
|
15
|
+
self.created_by: NodeProperty | None = None
|
|
16
|
+
self.updated_at: str | None = None
|
|
17
|
+
self.updated_by: NodeProperty | None = None
|
|
18
|
+
|
|
19
|
+
if data:
|
|
20
|
+
self.created_at = data.get("created_at")
|
|
21
|
+
self.updated_at = data.get("updated_at")
|
|
22
|
+
if data.get("created_by"):
|
|
23
|
+
self.created_by = NodeProperty(data["created_by"])
|
|
24
|
+
if data.get("updated_by"):
|
|
25
|
+
self.updated_by = NodeProperty(data["updated_by"])
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
f"NodeMetadata(created_at={self.created_at!r}, created_by={self.created_by!r}, "
|
|
30
|
+
f"updated_at={self.updated_at!r}, updated_by={self.updated_by!r})"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _generate_query_data(cls) -> dict:
|
|
35
|
+
"""Generate the query structure for node_metadata fields."""
|
|
36
|
+
return {
|
|
37
|
+
"created_at": None,
|
|
38
|
+
"created_by": {"id": None, "__typename": None, "display_label": None},
|
|
39
|
+
"updated_at": None,
|
|
40
|
+
"updated_by": {"id": None, "__typename": None, "display_label": None},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RelationshipMetadata:
|
|
45
|
+
"""Represents metadata about a relationship edge (updated_at, updated_by)."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, data: dict | None = None) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Args:
|
|
50
|
+
data: Data containing the metadata fields from the GraphQL response.
|
|
51
|
+
"""
|
|
52
|
+
self.updated_at: str | None = None
|
|
53
|
+
self.updated_by: NodeProperty | None = None
|
|
54
|
+
|
|
55
|
+
if data:
|
|
56
|
+
self.updated_at = data.get("updated_at")
|
|
57
|
+
if data.get("updated_by"):
|
|
58
|
+
self.updated_by = NodeProperty(data["updated_by"])
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
return f"RelationshipMetadata(updated_at={self.updated_at!r}, updated_by={self.updated_by!r})"
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def _generate_query_data(cls) -> dict:
|
|
65
|
+
"""Generate the query structure for relationship_metadata fields."""
|
|
66
|
+
return {
|
|
67
|
+
"updated_at": None,
|
|
68
|
+
"updated_by": {"id": None, "__typename": None, "display_label": None},
|
|
69
|
+
}
|
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
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
5
|
-
from ..exceptions import
|
|
6
|
-
Error,
|
|
7
|
-
)
|
|
6
|
+
from ..exceptions import Error
|
|
8
7
|
from ..protocols_base import CoreNodeBase
|
|
9
|
-
from .constants import PROPERTIES_FLAG, PROPERTIES_OBJECT
|
|
8
|
+
from .constants import PROFILE_KIND_PREFIX, PROPERTIES_FLAG, PROPERTIES_OBJECT
|
|
9
|
+
from .metadata import NodeMetadata, RelationshipMetadata
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from ..client import InfrahubClient, InfrahubClientSync
|
|
@@ -40,11 +40,14 @@ class RelatedNodeBase:
|
|
|
40
40
|
self._display_label: str | None = None
|
|
41
41
|
self._typename: str | None = None
|
|
42
42
|
self._kind: str | None = None
|
|
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}
|
|
@@ -74,11 +77,17 @@ class RelatedNodeBase:
|
|
|
74
77
|
prop_data = properties_data.get(prop, properties_data.get(f"_relation__{prop}", None))
|
|
75
78
|
if prop_data and isinstance(prop_data, dict) and "id" in prop_data:
|
|
76
79
|
setattr(self, prop, prop_data["id"])
|
|
80
|
+
if prop == "source" and "__typename" in prop_data:
|
|
81
|
+
self._source_typename = prop_data["__typename"]
|
|
77
82
|
elif prop_data and isinstance(prop_data, (str, bool)):
|
|
78
83
|
setattr(self, prop, prop_data)
|
|
79
84
|
else:
|
|
80
85
|
setattr(self, prop, None)
|
|
81
86
|
|
|
87
|
+
# Parse relationship metadata (at edge level)
|
|
88
|
+
if data.get("relationship_metadata"):
|
|
89
|
+
self._relationship_metadata = RelationshipMetadata(data["relationship_metadata"])
|
|
90
|
+
|
|
82
91
|
@property
|
|
83
92
|
def id(self) -> str | None:
|
|
84
93
|
if self._peer:
|
|
@@ -125,6 +134,17 @@ class RelatedNodeBase:
|
|
|
125
134
|
return self._peer.get_kind()
|
|
126
135
|
return self._kind
|
|
127
136
|
|
|
137
|
+
@property
|
|
138
|
+
def is_from_profile(self) -> bool:
|
|
139
|
+
"""Return whether this relationship was set from a profile. Done by checking if the source is of a profile kind."""
|
|
140
|
+
if not self._source_typename:
|
|
141
|
+
return False
|
|
142
|
+
return bool(re.match(rf"^{PROFILE_KIND_PREFIX}[A-Z]", self._source_typename))
|
|
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
|
+
|
|
128
148
|
def _generate_input_data(self, allocate_from_pool: bool = False) -> dict[str, Any]:
|
|
129
149
|
data: dict[str, Any] = {}
|
|
130
150
|
|
|
@@ -151,12 +171,17 @@ class RelatedNodeBase:
|
|
|
151
171
|
return {}
|
|
152
172
|
|
|
153
173
|
@classmethod
|
|
154
|
-
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:
|
|
155
177
|
"""Generates the basic structure of a GraphQL query for a single relationship.
|
|
156
178
|
|
|
157
179
|
Args:
|
|
158
180
|
peer_data (dict[str, Union[Any, Dict]], optional): Additional data to be included in the query for the node.
|
|
159
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.
|
|
160
185
|
|
|
161
186
|
Returns:
|
|
162
187
|
Dict: A dictionary representing the basic structure of a GraphQL query, including the node's ID, display label,
|
|
@@ -172,6 +197,13 @@ class RelatedNodeBase:
|
|
|
172
197
|
properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
|
|
173
198
|
|
|
174
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
|
+
|
|
175
207
|
if peer_data:
|
|
176
208
|
data["node"].update(peer_data)
|
|
177
209
|
|