infrahub-server 1.4.10__py3-none-any.whl → 1.5.0b1__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 +208 -16
- infrahub/api/artifact.py +3 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/api/query.py +2 -0
- infrahub/api/schema.py +3 -0
- infrahub/auth.py +5 -5
- infrahub/cli/db.py +26 -2
- infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
- infrahub/config.py +7 -2
- infrahub/core/attribute.py +25 -22
- infrahub/core/branch/models.py +2 -2
- infrahub/core/branch/needs_rebase_status.py +11 -0
- infrahub/core/branch/tasks.py +4 -3
- infrahub/core/changelog/models.py +4 -12
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/constants/infrahubkind.py +1 -0
- infrahub/core/convert_object_type/object_conversion.py +201 -0
- infrahub/core/convert_object_type/repository_conversion.py +89 -0
- infrahub/core/convert_object_type/schema_mapping.py +27 -3
- infrahub/core/diff/model/path.py +4 -0
- infrahub/core/diff/payload_builder.py +1 -1
- infrahub/core/diff/query/artifact.py +1 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +2 -2
- infrahub/core/ipam/utilization.py +1 -1
- infrahub/core/manager.py +9 -84
- infrahub/core/migrations/graph/__init__.py +6 -0
- infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
- infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
- infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
- infrahub/core/migrations/schema/node_attribute_add.py +5 -2
- infrahub/core/migrations/shared.py +5 -6
- infrahub/core/node/__init__.py +165 -42
- infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
- infrahub/core/node/create.py +67 -35
- infrahub/core/node/lock_utils.py +98 -0
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/standard.py +1 -1
- infrahub/core/property.py +11 -0
- infrahub/core/protocols.py +8 -1
- infrahub/core/query/attribute.py +27 -15
- infrahub/core/query/node.py +61 -185
- infrahub/core/query/relationship.py +43 -26
- infrahub/core/query/subquery.py +0 -8
- infrahub/core/registry.py +2 -2
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/model.py +60 -20
- infrahub/core/schema/attribute_schema.py +0 -2
- infrahub/core/schema/basenode_schema.py +42 -2
- infrahub/core/schema/definitions/core/__init__.py +2 -0
- infrahub/core/schema/definitions/core/generator.py +2 -0
- infrahub/core/schema/definitions/core/group.py +16 -2
- infrahub/core/schema/definitions/core/repository.py +7 -0
- infrahub/core/schema/definitions/internal.py +14 -1
- infrahub/core/schema/generated/base_node_schema.py +6 -1
- infrahub/core/schema/node_schema.py +5 -2
- infrahub/core/schema/relationship_schema.py +0 -1
- infrahub/core/schema/schema_branch.py +137 -2
- infrahub/core/schema/schema_branch_display.py +123 -0
- infrahub/core/schema/schema_branch_hfid.py +114 -0
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/determiner.py +12 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/core/validators/tasks.py +1 -1
- infrahub/display_labels/__init__.py +0 -0
- infrahub/display_labels/gather.py +48 -0
- infrahub/display_labels/models.py +240 -0
- infrahub/display_labels/tasks.py +186 -0
- infrahub/display_labels/triggers.py +22 -0
- infrahub/events/group_action.py +1 -1
- infrahub/events/node_action.py +1 -1
- infrahub/generators/constants.py +7 -0
- infrahub/generators/models.py +38 -12
- infrahub/generators/tasks.py +34 -16
- infrahub/git/base.py +38 -1
- infrahub/git/integrator.py +22 -14
- infrahub/graphql/analyzer.py +1 -1
- infrahub/graphql/api/dependencies.py +2 -4
- infrahub/graphql/api/endpoints.py +2 -2
- infrahub/graphql/app.py +2 -4
- infrahub/graphql/initialization.py +2 -3
- infrahub/graphql/manager.py +212 -137
- infrahub/graphql/middleware.py +12 -0
- infrahub/graphql/mutations/branch.py +11 -0
- infrahub/graphql/mutations/computed_attribute.py +110 -3
- infrahub/graphql/mutations/convert_object_type.py +34 -13
- infrahub/graphql/mutations/display_label.py +111 -0
- infrahub/graphql/mutations/generator.py +25 -7
- infrahub/graphql/mutations/hfid.py +118 -0
- infrahub/graphql/mutations/ipam.py +21 -8
- infrahub/graphql/mutations/main.py +37 -153
- infrahub/graphql/mutations/profile.py +195 -0
- infrahub/graphql/mutations/proposed_change.py +2 -1
- infrahub/graphql/mutations/relationship.py +2 -2
- infrahub/graphql/mutations/repository.py +22 -83
- infrahub/graphql/mutations/resource_manager.py +2 -2
- infrahub/graphql/mutations/schema.py +5 -5
- infrahub/graphql/mutations/webhook.py +1 -1
- infrahub/graphql/queries/resource_manager.py +1 -1
- infrahub/graphql/registry.py +173 -0
- infrahub/graphql/resolvers/resolver.py +2 -0
- infrahub/graphql/schema.py +8 -1
- infrahub/groups/tasks.py +1 -1
- infrahub/hfid/__init__.py +0 -0
- infrahub/hfid/gather.py +48 -0
- infrahub/hfid/models.py +240 -0
- infrahub/hfid/tasks.py +185 -0
- infrahub/hfid/triggers.py +22 -0
- infrahub/lock.py +67 -30
- infrahub/locks/__init__.py +0 -0
- infrahub/locks/tasks.py +37 -0
- infrahub/middleware.py +26 -1
- infrahub/patch/plan_writer.py +2 -2
- infrahub/profiles/__init__.py +0 -0
- infrahub/profiles/node_applier.py +101 -0
- infrahub/profiles/queries/__init__.py +0 -0
- infrahub/profiles/queries/get_profile_data.py +99 -0
- infrahub/profiles/tasks.py +63 -0
- infrahub/proposed_change/tasks.py +10 -1
- infrahub/repositories/__init__.py +0 -0
- infrahub/repositories/create_repository.py +113 -0
- infrahub/server.py +16 -3
- infrahub/services/__init__.py +8 -5
- infrahub/tasks/registry.py +6 -4
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/tasks.py +3 -0
- infrahub/webhook/models.py +1 -1
- infrahub/workflows/catalogue.py +110 -3
- infrahub/workflows/initialization.py +16 -0
- infrahub/workflows/models.py +17 -2
- infrahub_sdk/branch.py +5 -8
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/client.py +364 -84
- infrahub_sdk/convert_object_type.py +61 -0
- infrahub_sdk/ctl/check.py +2 -3
- infrahub_sdk/ctl/cli_commands.py +18 -12
- infrahub_sdk/ctl/config.py +8 -2
- infrahub_sdk/ctl/generator.py +6 -3
- infrahub_sdk/ctl/graphql.py +184 -0
- infrahub_sdk/ctl/repository.py +39 -1
- infrahub_sdk/ctl/schema.py +18 -3
- infrahub_sdk/ctl/utils.py +4 -0
- infrahub_sdk/ctl/validate.py +5 -3
- infrahub_sdk/diff.py +4 -5
- infrahub_sdk/exceptions.py +2 -0
- infrahub_sdk/generator.py +7 -1
- infrahub_sdk/graphql/__init__.py +12 -0
- infrahub_sdk/graphql/constants.py +1 -0
- infrahub_sdk/graphql/plugin.py +85 -0
- infrahub_sdk/graphql/query.py +77 -0
- infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
- infrahub_sdk/graphql/utils.py +40 -0
- infrahub_sdk/node/attribute.py +2 -0
- infrahub_sdk/node/node.py +28 -20
- infrahub_sdk/playback.py +1 -2
- infrahub_sdk/protocols.py +54 -6
- infrahub_sdk/pytest_plugin/plugin.py +7 -4
- infrahub_sdk/pytest_plugin/utils.py +40 -0
- infrahub_sdk/repository.py +1 -2
- infrahub_sdk/schema/__init__.py +38 -0
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/object.py +120 -7
- infrahub_sdk/spec/range_expansion.py +118 -0
- infrahub_sdk/timestamp.py +18 -6
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +9 -11
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +177 -134
- infrahub_testcontainers/container.py +1 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- infrahub_testcontainers/models.py +2 -2
- infrahub_testcontainers/performance_test.py +4 -4
- infrahub/core/convert_object_type/conversion.py +0 -134
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from ariadne_codegen.plugins.base import Plugin
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from graphql import ExecutableDefinitionNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FutureAnnotationPlugin(Plugin):
|
|
13
|
+
@staticmethod
|
|
14
|
+
def insert_future_annotation(module: ast.Module) -> ast.Module:
|
|
15
|
+
# First check if the future annotation is already present
|
|
16
|
+
for item in module.body:
|
|
17
|
+
if isinstance(item, ast.ImportFrom) and item.module == "__future__":
|
|
18
|
+
if any(alias.name == "annotations" for alias in item.names):
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
module.body.insert(0, ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0))
|
|
22
|
+
return module
|
|
23
|
+
|
|
24
|
+
def generate_result_types_module(
|
|
25
|
+
self,
|
|
26
|
+
module: ast.Module,
|
|
27
|
+
operation_definition: ExecutableDefinitionNode, # noqa: ARG002
|
|
28
|
+
) -> ast.Module:
|
|
29
|
+
return self.insert_future_annotation(module)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StandardTypeHintPlugin(Plugin):
|
|
33
|
+
@classmethod
|
|
34
|
+
def replace_list_in_subscript(cls, subscript: ast.Subscript) -> ast.Subscript:
|
|
35
|
+
if isinstance(subscript.value, ast.Name) and subscript.value.id == "List":
|
|
36
|
+
subscript.value.id = "list"
|
|
37
|
+
if isinstance(subscript.slice, ast.Subscript):
|
|
38
|
+
subscript.slice = cls.replace_list_in_subscript(subscript.slice)
|
|
39
|
+
|
|
40
|
+
return subscript
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def replace_list_annotations(cls, module: ast.Module) -> ast.Module:
|
|
44
|
+
for item in module.body:
|
|
45
|
+
if not isinstance(item, ast.ClassDef):
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# replace List with list in the annotations when list is used as a type
|
|
49
|
+
for class_item in item.body:
|
|
50
|
+
if not isinstance(class_item, ast.AnnAssign):
|
|
51
|
+
continue
|
|
52
|
+
if isinstance(class_item.annotation, ast.Subscript):
|
|
53
|
+
class_item.annotation = cls.replace_list_in_subscript(class_item.annotation)
|
|
54
|
+
|
|
55
|
+
return module
|
|
56
|
+
|
|
57
|
+
def generate_result_types_module(
|
|
58
|
+
self,
|
|
59
|
+
module: ast.Module,
|
|
60
|
+
operation_definition: ExecutableDefinitionNode, # noqa: ARG002
|
|
61
|
+
) -> ast.Module:
|
|
62
|
+
module = FutureAnnotationPlugin.insert_future_annotation(module)
|
|
63
|
+
return self.replace_list_annotations(module)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PydanticBaseModelPlugin(Plugin):
|
|
67
|
+
@staticmethod
|
|
68
|
+
def find_base_model_index(module: ast.Module) -> int:
|
|
69
|
+
for idx, item in enumerate(module.body):
|
|
70
|
+
if isinstance(item, ast.ImportFrom) and item.module == "base_model":
|
|
71
|
+
return idx
|
|
72
|
+
raise ValueError("BaseModel not found in module")
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def replace_base_model_import(cls, module: ast.Module) -> ast.Module:
|
|
76
|
+
base_model_index = cls.find_base_model_index(module)
|
|
77
|
+
module.body[base_model_index] = ast.ImportFrom(module="pydantic", names=[ast.alias(name="BaseModel")], level=0)
|
|
78
|
+
return module
|
|
79
|
+
|
|
80
|
+
def generate_result_types_module(
|
|
81
|
+
self,
|
|
82
|
+
module: ast.Module,
|
|
83
|
+
operation_definition: ExecutableDefinitionNode, # noqa: ARG002
|
|
84
|
+
) -> ast.Module:
|
|
85
|
+
return self.replace_base_model_import(module)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .renderers import render_input_block, render_query_block, render_variables_to_string
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseGraphQLQuery:
|
|
9
|
+
query_type: str = "not-defined"
|
|
10
|
+
indentation: int = 4
|
|
11
|
+
|
|
12
|
+
def __init__(self, query: dict, variables: dict | None = None, name: str | None = None):
|
|
13
|
+
self.query = query
|
|
14
|
+
self.variables = variables
|
|
15
|
+
self.name = name or ""
|
|
16
|
+
|
|
17
|
+
def render_first_line(self) -> str:
|
|
18
|
+
first_line = self.query_type
|
|
19
|
+
|
|
20
|
+
if self.name:
|
|
21
|
+
first_line += " " + self.name
|
|
22
|
+
|
|
23
|
+
if self.variables:
|
|
24
|
+
first_line += f" ({render_variables_to_string(self.variables)})"
|
|
25
|
+
|
|
26
|
+
first_line += " {"
|
|
27
|
+
|
|
28
|
+
return first_line
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Query(BaseGraphQLQuery):
|
|
32
|
+
query_type = "query"
|
|
33
|
+
|
|
34
|
+
def render(self, convert_enum: bool = False) -> str:
|
|
35
|
+
lines = [self.render_first_line()]
|
|
36
|
+
lines.extend(
|
|
37
|
+
render_query_block(
|
|
38
|
+
data=self.query, indentation=self.indentation, offset=self.indentation, convert_enum=convert_enum
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
lines.append("}")
|
|
42
|
+
|
|
43
|
+
return "\n" + "\n".join(lines) + "\n"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Mutation(BaseGraphQLQuery):
|
|
47
|
+
query_type = "mutation"
|
|
48
|
+
|
|
49
|
+
def __init__(self, *args: Any, mutation: str, input_data: dict, **kwargs: Any):
|
|
50
|
+
self.input_data = input_data
|
|
51
|
+
self.mutation = mutation
|
|
52
|
+
super().__init__(*args, **kwargs)
|
|
53
|
+
|
|
54
|
+
def render(self, convert_enum: bool = False) -> str:
|
|
55
|
+
lines = [self.render_first_line()]
|
|
56
|
+
lines.append(" " * self.indentation + f"{self.mutation}(")
|
|
57
|
+
lines.extend(
|
|
58
|
+
render_input_block(
|
|
59
|
+
data=self.input_data,
|
|
60
|
+
indentation=self.indentation,
|
|
61
|
+
offset=self.indentation * 2,
|
|
62
|
+
convert_enum=convert_enum,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
lines.append(" " * self.indentation + "){")
|
|
66
|
+
lines.extend(
|
|
67
|
+
render_query_block(
|
|
68
|
+
data=self.query,
|
|
69
|
+
indentation=self.indentation,
|
|
70
|
+
offset=self.indentation * 2,
|
|
71
|
+
convert_enum=convert_enum,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
lines.append(" " * self.indentation + "}")
|
|
75
|
+
lines.append("}")
|
|
76
|
+
|
|
77
|
+
return "\n" + "\n".join(lines) + "\n"
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
from .constants import VARIABLE_TYPE_MAPPING
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def convert_to_graphql_as_string(value:
|
|
12
|
+
def convert_to_graphql_as_string(value: Any, convert_enum: bool = False) -> str: # noqa: PLR0911
|
|
13
|
+
"""Convert a Python value to its GraphQL string representation.
|
|
14
|
+
|
|
15
|
+
This function handles various Python types and converts them to their appropriate
|
|
16
|
+
GraphQL string format, including proper quoting, formatting, and special handling
|
|
17
|
+
for different data types.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
value: The value to convert to GraphQL string format. Can be None, str, bool,
|
|
21
|
+
int, float, Enum, list, BaseModel, or any other type.
|
|
22
|
+
convert_enum: If True, converts Enum values to their underlying value instead
|
|
23
|
+
of their name. Defaults to False.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
str: The GraphQL string representation of the value.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> convert_to_graphql_as_string("hello")
|
|
30
|
+
'"hello"'
|
|
31
|
+
>>> convert_to_graphql_as_string(True)
|
|
32
|
+
'true'
|
|
33
|
+
>>> convert_to_graphql_as_string([1, 2, 3])
|
|
34
|
+
'[1, 2, 3]'
|
|
35
|
+
>>> convert_to_graphql_as_string(None)
|
|
36
|
+
'null'
|
|
37
|
+
"""
|
|
38
|
+
if value is None:
|
|
39
|
+
return "null"
|
|
12
40
|
if isinstance(value, str) and value.startswith("$"):
|
|
13
41
|
return value
|
|
14
42
|
if isinstance(value, Enum):
|
|
@@ -16,7 +44,9 @@ def convert_to_graphql_as_string(value: str | bool | list | BaseModel | Enum | A
|
|
|
16
44
|
return convert_to_graphql_as_string(value=value.value, convert_enum=True)
|
|
17
45
|
return value.name
|
|
18
46
|
if isinstance(value, str):
|
|
19
|
-
|
|
47
|
+
# Use json.dumps() to properly escape the string according to JSON rules,
|
|
48
|
+
# which are compatible with GraphQL string escaping
|
|
49
|
+
return json.dumps(value)
|
|
20
50
|
if isinstance(value, bool):
|
|
21
51
|
return repr(value).lower()
|
|
22
52
|
if isinstance(value, list):
|
|
@@ -51,6 +81,34 @@ def render_variables_to_string(data: dict[str, type[str | int | float | bool]])
|
|
|
51
81
|
|
|
52
82
|
|
|
53
83
|
def render_query_block(data: dict, offset: int = 4, indentation: int = 4, convert_enum: bool = False) -> list[str]:
|
|
84
|
+
"""Render a dictionary structure as a GraphQL query block with proper formatting.
|
|
85
|
+
|
|
86
|
+
This function recursively processes a dictionary to generate GraphQL query syntax
|
|
87
|
+
with proper indentation, handling of aliases, filters, and nested structures.
|
|
88
|
+
Special keys like "@filters" and "@alias" are processed for GraphQL-specific
|
|
89
|
+
formatting.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
data: Dictionary representing the GraphQL query structure. Can contain
|
|
93
|
+
nested dictionaries, special keys like "@filters" and "@alias", and
|
|
94
|
+
various value types.
|
|
95
|
+
offset: Number of spaces to use for initial indentation. Defaults to 4.
|
|
96
|
+
indentation: Number of spaces to add for each nesting level. Defaults to 4.
|
|
97
|
+
convert_enum: If True, converts Enum values to their underlying value.
|
|
98
|
+
Defaults to False.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
list[str]: List of formatted lines representing the GraphQL query block.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
>>> data = {"user": {"name": None, "email": None}}
|
|
105
|
+
>>> render_query_block(data)
|
|
106
|
+
[' user {', ' name', ' email', ' }']
|
|
107
|
+
|
|
108
|
+
>>> data = {"user": {"@alias": "u", "@filters": {"id": 123}, "name": None}}
|
|
109
|
+
>>> render_query_block(data)
|
|
110
|
+
[' u: user(id: 123) {', ' name', ' }']
|
|
111
|
+
"""
|
|
54
112
|
FILTERS_KEY = "@filters"
|
|
55
113
|
ALIAS_KEY = "@alias"
|
|
56
114
|
KEYWORDS_TO_SKIP = [FILTERS_KEY, ALIAS_KEY]
|
|
@@ -92,6 +150,33 @@ def render_query_block(data: dict, offset: int = 4, indentation: int = 4, conver
|
|
|
92
150
|
|
|
93
151
|
|
|
94
152
|
def render_input_block(data: dict, offset: int = 4, indentation: int = 4, convert_enum: bool = False) -> list[str]:
|
|
153
|
+
"""Render a dictionary structure as a GraphQL input block with proper formatting.
|
|
154
|
+
|
|
155
|
+
This function recursively processes a dictionary to generate GraphQL input syntax
|
|
156
|
+
with proper indentation, handling nested objects, arrays, and various data types.
|
|
157
|
+
Unlike query blocks, input blocks don't handle special keys like "@filters" or
|
|
158
|
+
"@alias" and focus on data structure representation.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
data: Dictionary representing the GraphQL input structure. Can contain
|
|
162
|
+
nested dictionaries, lists, and various value types.
|
|
163
|
+
offset: Number of spaces to use for initial indentation. Defaults to 4.
|
|
164
|
+
indentation: Number of spaces to add for each nesting level. Defaults to 4.
|
|
165
|
+
convert_enum: If True, converts Enum values to their underlying value.
|
|
166
|
+
Defaults to False.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
list[str]: List of formatted lines representing the GraphQL input block.
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
>>> data = {"name": "John", "age": 30}
|
|
173
|
+
>>> render_input_block(data)
|
|
174
|
+
[' name: "John"', ' age: 30']
|
|
175
|
+
|
|
176
|
+
>>> data = {"user": {"name": "John", "hobbies": ["reading", "coding"]}}
|
|
177
|
+
>>> render_input_block(data)
|
|
178
|
+
[' user: {', ' name: "John"', ' hobbies: [', ' "reading",', ' "coding",', ' ]', ' }']
|
|
179
|
+
"""
|
|
95
180
|
offset_str = " " * offset
|
|
96
181
|
lines = []
|
|
97
182
|
for key, value in data.items():
|
|
@@ -125,75 +210,3 @@ def render_input_block(data: dict, offset: int = 4, indentation: int = 4, conver
|
|
|
125
210
|
else:
|
|
126
211
|
lines.append(f"{offset_str}{key}: {convert_to_graphql_as_string(value=value, convert_enum=convert_enum)}")
|
|
127
212
|
return lines
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class BaseGraphQLQuery:
|
|
131
|
-
query_type: str = "not-defined"
|
|
132
|
-
indentation: int = 4
|
|
133
|
-
|
|
134
|
-
def __init__(self, query: dict, variables: dict | None = None, name: str | None = None):
|
|
135
|
-
self.query = query
|
|
136
|
-
self.variables = variables
|
|
137
|
-
self.name = name or ""
|
|
138
|
-
|
|
139
|
-
def render_first_line(self) -> str:
|
|
140
|
-
first_line = self.query_type
|
|
141
|
-
|
|
142
|
-
if self.name:
|
|
143
|
-
first_line += " " + self.name
|
|
144
|
-
|
|
145
|
-
if self.variables:
|
|
146
|
-
first_line += f" ({render_variables_to_string(self.variables)})"
|
|
147
|
-
|
|
148
|
-
first_line += " {"
|
|
149
|
-
|
|
150
|
-
return first_line
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class Query(BaseGraphQLQuery):
|
|
154
|
-
query_type = "query"
|
|
155
|
-
|
|
156
|
-
def render(self, convert_enum: bool = False) -> str:
|
|
157
|
-
lines = [self.render_first_line()]
|
|
158
|
-
lines.extend(
|
|
159
|
-
render_query_block(
|
|
160
|
-
data=self.query, indentation=self.indentation, offset=self.indentation, convert_enum=convert_enum
|
|
161
|
-
)
|
|
162
|
-
)
|
|
163
|
-
lines.append("}")
|
|
164
|
-
|
|
165
|
-
return "\n" + "\n".join(lines) + "\n"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class Mutation(BaseGraphQLQuery):
|
|
169
|
-
query_type = "mutation"
|
|
170
|
-
|
|
171
|
-
def __init__(self, *args: Any, mutation: str, input_data: dict, **kwargs: Any):
|
|
172
|
-
self.input_data = input_data
|
|
173
|
-
self.mutation = mutation
|
|
174
|
-
super().__init__(*args, **kwargs)
|
|
175
|
-
|
|
176
|
-
def render(self, convert_enum: bool = False) -> str:
|
|
177
|
-
lines = [self.render_first_line()]
|
|
178
|
-
lines.append(" " * self.indentation + f"{self.mutation}(")
|
|
179
|
-
lines.extend(
|
|
180
|
-
render_input_block(
|
|
181
|
-
data=self.input_data,
|
|
182
|
-
indentation=self.indentation,
|
|
183
|
-
offset=self.indentation * 2,
|
|
184
|
-
convert_enum=convert_enum,
|
|
185
|
-
)
|
|
186
|
-
)
|
|
187
|
-
lines.append(" " * self.indentation + "){")
|
|
188
|
-
lines.extend(
|
|
189
|
-
render_query_block(
|
|
190
|
-
data=self.query,
|
|
191
|
-
indentation=self.indentation,
|
|
192
|
-
offset=self.indentation * 2,
|
|
193
|
-
convert_enum=convert_enum,
|
|
194
|
-
)
|
|
195
|
-
)
|
|
196
|
-
lines.append(" " * self.indentation + "}")
|
|
197
|
-
lines.append("}")
|
|
198
|
-
|
|
199
|
-
return "\n" + "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_class_def_index(module: ast.Module) -> int:
|
|
5
|
+
"""Get the index of the first class definition in the module.
|
|
6
|
+
It's useful to insert other classes before the first class definition."""
|
|
7
|
+
for idx, item in enumerate(module.body):
|
|
8
|
+
if isinstance(item, ast.ClassDef):
|
|
9
|
+
return idx
|
|
10
|
+
return -1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def insert_fragments_inline(module: ast.Module, fragment: ast.Module) -> ast.Module:
|
|
14
|
+
"""Insert the Pydantic classes for the fragments inline into the module.
|
|
15
|
+
|
|
16
|
+
If no class definitions exist in module, fragments are appended to the end.
|
|
17
|
+
"""
|
|
18
|
+
module_class_def_index = get_class_def_index(module)
|
|
19
|
+
|
|
20
|
+
fragment_classes: list[ast.ClassDef] = [item for item in fragment.body if isinstance(item, ast.ClassDef)]
|
|
21
|
+
|
|
22
|
+
# Handle edge case when no class definitions exist
|
|
23
|
+
if module_class_def_index == -1:
|
|
24
|
+
# Append fragments to the end of the module
|
|
25
|
+
module.body.extend(fragment_classes)
|
|
26
|
+
else:
|
|
27
|
+
# Insert fragments before the first class definition
|
|
28
|
+
for idx, item in enumerate(fragment_classes):
|
|
29
|
+
module.body.insert(module_class_def_index + idx, item)
|
|
30
|
+
|
|
31
|
+
return module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def remove_fragment_import(module: ast.Module) -> ast.Module:
|
|
35
|
+
"""Remove the fragment import from the module."""
|
|
36
|
+
for item in module.body:
|
|
37
|
+
if isinstance(item, ast.ImportFrom) and item.module == "fragments":
|
|
38
|
+
module.body.remove(item)
|
|
39
|
+
return module
|
|
40
|
+
return module
|
infrahub_sdk/node/attribute.py
CHANGED
infrahub_sdk/node/node.py
CHANGED
|
@@ -234,15 +234,10 @@ class InfrahubNodeBase:
|
|
|
234
234
|
|
|
235
235
|
rel: RelatedNodeBase | RelationshipManagerBase = getattr(self, item_name)
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
# and self._schema.get_relationship(item_name).cardinality == "one"
|
|
242
|
-
# ):
|
|
243
|
-
# data[item_name] = None
|
|
244
|
-
# continue
|
|
245
|
-
# el
|
|
237
|
+
if rel_schema.cardinality == RelationshipCardinality.ONE and rel_schema.optional and not rel.initialized:
|
|
238
|
+
data[item_name] = None
|
|
239
|
+
continue
|
|
240
|
+
|
|
246
241
|
if rel is None or not rel.initialized:
|
|
247
242
|
continue
|
|
248
243
|
|
|
@@ -315,7 +310,16 @@ class InfrahubNodeBase:
|
|
|
315
310
|
variables.pop(variable_key)
|
|
316
311
|
|
|
317
312
|
# TODO: I do not feel _great_ about this
|
|
318
|
-
|
|
313
|
+
# -> I don't even know who you are (but this is not great indeed) -- gmazoyer (quoting Thanos)
|
|
314
|
+
original_data_item = original_data.get(item)
|
|
315
|
+
original_data_item_is_none = original_data_item is None
|
|
316
|
+
if isinstance(original_data_item, dict):
|
|
317
|
+
if "node" in original_data_item:
|
|
318
|
+
original_data_item_is_none = original_data_item["node"] is None
|
|
319
|
+
elif "id" not in original_data_item:
|
|
320
|
+
original_data_item_is_none = True
|
|
321
|
+
|
|
322
|
+
if item in data and (data_item in ({}, []) or (data_item is None and original_data_item_is_none)):
|
|
319
323
|
data.pop(item)
|
|
320
324
|
|
|
321
325
|
def _strip_unmodified(self, data: dict, variables: dict) -> tuple[dict, dict]:
|
|
@@ -324,7 +328,9 @@ class InfrahubNodeBase:
|
|
|
324
328
|
relationship_property = getattr(self, relationship)
|
|
325
329
|
if not relationship_property or relationship not in data:
|
|
326
330
|
continue
|
|
327
|
-
if not relationship_property.initialized
|
|
331
|
+
if not relationship_property.initialized and (
|
|
332
|
+
not isinstance(relationship_property, RelatedNodeBase) or not relationship_property.schema.optional
|
|
333
|
+
):
|
|
328
334
|
data.pop(relationship)
|
|
329
335
|
elif isinstance(relationship_property, RelationshipManagerBase) and not relationship_property.has_update:
|
|
330
336
|
data.pop(relationship)
|
|
@@ -573,8 +579,7 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
573
579
|
self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
|
|
574
580
|
|
|
575
581
|
artifact = await self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
|
|
576
|
-
|
|
577
|
-
return content
|
|
582
|
+
return await self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
|
|
578
583
|
|
|
579
584
|
async def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
|
|
580
585
|
input_data = {"data": {"id": self.id}}
|
|
@@ -742,12 +747,11 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
742
747
|
continue
|
|
743
748
|
|
|
744
749
|
peer_data: dict[str, Any] = {}
|
|
745
|
-
|
|
750
|
+
should_fetch_relationship = prefetch_relationships or (include is not None and rel_name in include)
|
|
751
|
+
if rel_schema and should_fetch_relationship:
|
|
746
752
|
peer_schema = await self._client.schema.get(kind=rel_schema.peer, branch=self._branch)
|
|
747
753
|
peer_node = InfrahubNode(client=self._client, schema=peer_schema, branch=self._branch)
|
|
748
754
|
peer_data = await peer_node.generate_query_data_node(
|
|
749
|
-
include=include,
|
|
750
|
-
exclude=exclude,
|
|
751
755
|
property=property,
|
|
752
756
|
)
|
|
753
757
|
|
|
@@ -886,7 +890,11 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
886
890
|
await self._process_mutation_result(mutation_name=mutation_name, response=response, timeout=timeout)
|
|
887
891
|
|
|
888
892
|
async def _process_relationships(
|
|
889
|
-
self,
|
|
893
|
+
self,
|
|
894
|
+
node_data: dict[str, Any],
|
|
895
|
+
branch: str,
|
|
896
|
+
related_nodes: list[InfrahubNode],
|
|
897
|
+
timeout: int | None = None,
|
|
890
898
|
) -> None:
|
|
891
899
|
"""Processes the Relationships of a InfrahubNode and add Related Nodes to a list.
|
|
892
900
|
|
|
@@ -1199,8 +1207,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1199
1207
|
def artifact_fetch(self, name: str) -> str | dict[str, Any]:
|
|
1200
1208
|
self._validate_artifact_support(ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE)
|
|
1201
1209
|
artifact = self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
|
|
1202
|
-
|
|
1203
|
-
return content
|
|
1210
|
+
return self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
|
|
1204
1211
|
|
|
1205
1212
|
def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
|
|
1206
1213
|
input_data = {"data": {"id": self.id}}
|
|
@@ -1363,7 +1370,8 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1363
1370
|
continue
|
|
1364
1371
|
|
|
1365
1372
|
peer_data: dict[str, Any] = {}
|
|
1366
|
-
|
|
1373
|
+
should_fetch_relationship = prefetch_relationships or (include is not None and rel_name in include)
|
|
1374
|
+
if rel_schema and should_fetch_relationship:
|
|
1367
1375
|
peer_schema = self._client.schema.get(kind=rel_schema.peer, branch=self._branch)
|
|
1368
1376
|
peer_node = InfrahubNodeSync(client=self._client, schema=peer_schema, branch=self._branch)
|
|
1369
1377
|
peer_data = peer_node.generate_query_data_node(include=include, exclude=exclude, property=property)
|
infrahub_sdk/playback.py
CHANGED
|
@@ -56,5 +56,4 @@ class JSONPlayback(BaseSettings):
|
|
|
56
56
|
with Path(f"{self.directory}/{filename}.json").open(encoding="utf-8") as fobj:
|
|
57
57
|
data = ujson.load(fobj)
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
return response
|
|
59
|
+
return httpx.Response(status_code=data["status_code"], content=data["response_content"], request=request)
|