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.
Files changed (178) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/query.py +2 -0
  5. infrahub/api/schema.py +3 -0
  6. infrahub/auth.py +5 -5
  7. infrahub/cli/db.py +26 -2
  8. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  9. infrahub/config.py +7 -2
  10. infrahub/core/attribute.py +25 -22
  11. infrahub/core/branch/models.py +2 -2
  12. infrahub/core/branch/needs_rebase_status.py +11 -0
  13. infrahub/core/branch/tasks.py +4 -3
  14. infrahub/core/changelog/models.py +4 -12
  15. infrahub/core/constants/__init__.py +1 -0
  16. infrahub/core/constants/infrahubkind.py +1 -0
  17. infrahub/core/convert_object_type/object_conversion.py +201 -0
  18. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  19. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  20. infrahub/core/diff/model/path.py +4 -0
  21. infrahub/core/diff/payload_builder.py +1 -1
  22. infrahub/core/diff/query/artifact.py +1 -1
  23. infrahub/core/graph/__init__.py +1 -1
  24. infrahub/core/initialization.py +2 -2
  25. infrahub/core/ipam/utilization.py +1 -1
  26. infrahub/core/manager.py +9 -84
  27. infrahub/core/migrations/graph/__init__.py +6 -0
  28. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  29. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
  30. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
  31. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  32. infrahub/core/migrations/shared.py +5 -6
  33. infrahub/core/node/__init__.py +165 -42
  34. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  35. infrahub/core/node/create.py +67 -35
  36. infrahub/core/node/lock_utils.py +98 -0
  37. infrahub/core/node/node_property_attribute.py +230 -0
  38. infrahub/core/node/standard.py +1 -1
  39. infrahub/core/property.py +11 -0
  40. infrahub/core/protocols.py +8 -1
  41. infrahub/core/query/attribute.py +27 -15
  42. infrahub/core/query/node.py +61 -185
  43. infrahub/core/query/relationship.py +43 -26
  44. infrahub/core/query/subquery.py +0 -8
  45. infrahub/core/registry.py +2 -2
  46. infrahub/core/relationship/constraints/count.py +1 -1
  47. infrahub/core/relationship/model.py +60 -20
  48. infrahub/core/schema/attribute_schema.py +0 -2
  49. infrahub/core/schema/basenode_schema.py +42 -2
  50. infrahub/core/schema/definitions/core/__init__.py +2 -0
  51. infrahub/core/schema/definitions/core/generator.py +2 -0
  52. infrahub/core/schema/definitions/core/group.py +16 -2
  53. infrahub/core/schema/definitions/core/repository.py +7 -0
  54. infrahub/core/schema/definitions/internal.py +14 -1
  55. infrahub/core/schema/generated/base_node_schema.py +6 -1
  56. infrahub/core/schema/node_schema.py +5 -2
  57. infrahub/core/schema/relationship_schema.py +0 -1
  58. infrahub/core/schema/schema_branch.py +137 -2
  59. infrahub/core/schema/schema_branch_display.py +123 -0
  60. infrahub/core/schema/schema_branch_hfid.py +114 -0
  61. infrahub/core/validators/aggregated_checker.py +1 -1
  62. infrahub/core/validators/determiner.py +12 -1
  63. infrahub/core/validators/relationship/peer.py +1 -1
  64. infrahub/core/validators/tasks.py +1 -1
  65. infrahub/display_labels/__init__.py +0 -0
  66. infrahub/display_labels/gather.py +48 -0
  67. infrahub/display_labels/models.py +240 -0
  68. infrahub/display_labels/tasks.py +186 -0
  69. infrahub/display_labels/triggers.py +22 -0
  70. infrahub/events/group_action.py +1 -1
  71. infrahub/events/node_action.py +1 -1
  72. infrahub/generators/constants.py +7 -0
  73. infrahub/generators/models.py +38 -12
  74. infrahub/generators/tasks.py +34 -16
  75. infrahub/git/base.py +38 -1
  76. infrahub/git/integrator.py +22 -14
  77. infrahub/graphql/analyzer.py +1 -1
  78. infrahub/graphql/api/dependencies.py +2 -4
  79. infrahub/graphql/api/endpoints.py +2 -2
  80. infrahub/graphql/app.py +2 -4
  81. infrahub/graphql/initialization.py +2 -3
  82. infrahub/graphql/manager.py +212 -137
  83. infrahub/graphql/middleware.py +12 -0
  84. infrahub/graphql/mutations/branch.py +11 -0
  85. infrahub/graphql/mutations/computed_attribute.py +110 -3
  86. infrahub/graphql/mutations/convert_object_type.py +34 -13
  87. infrahub/graphql/mutations/display_label.py +111 -0
  88. infrahub/graphql/mutations/generator.py +25 -7
  89. infrahub/graphql/mutations/hfid.py +118 -0
  90. infrahub/graphql/mutations/ipam.py +21 -8
  91. infrahub/graphql/mutations/main.py +37 -153
  92. infrahub/graphql/mutations/profile.py +195 -0
  93. infrahub/graphql/mutations/proposed_change.py +2 -1
  94. infrahub/graphql/mutations/relationship.py +2 -2
  95. infrahub/graphql/mutations/repository.py +22 -83
  96. infrahub/graphql/mutations/resource_manager.py +2 -2
  97. infrahub/graphql/mutations/schema.py +5 -5
  98. infrahub/graphql/mutations/webhook.py +1 -1
  99. infrahub/graphql/queries/resource_manager.py +1 -1
  100. infrahub/graphql/registry.py +173 -0
  101. infrahub/graphql/resolvers/resolver.py +2 -0
  102. infrahub/graphql/schema.py +8 -1
  103. infrahub/groups/tasks.py +1 -1
  104. infrahub/hfid/__init__.py +0 -0
  105. infrahub/hfid/gather.py +48 -0
  106. infrahub/hfid/models.py +240 -0
  107. infrahub/hfid/tasks.py +185 -0
  108. infrahub/hfid/triggers.py +22 -0
  109. infrahub/lock.py +67 -30
  110. infrahub/locks/__init__.py +0 -0
  111. infrahub/locks/tasks.py +37 -0
  112. infrahub/middleware.py +26 -1
  113. infrahub/patch/plan_writer.py +2 -2
  114. infrahub/profiles/__init__.py +0 -0
  115. infrahub/profiles/node_applier.py +101 -0
  116. infrahub/profiles/queries/__init__.py +0 -0
  117. infrahub/profiles/queries/get_profile_data.py +99 -0
  118. infrahub/profiles/tasks.py +63 -0
  119. infrahub/proposed_change/tasks.py +10 -1
  120. infrahub/repositories/__init__.py +0 -0
  121. infrahub/repositories/create_repository.py +113 -0
  122. infrahub/server.py +16 -3
  123. infrahub/services/__init__.py +8 -5
  124. infrahub/tasks/registry.py +6 -4
  125. infrahub/trigger/catalogue.py +4 -0
  126. infrahub/trigger/models.py +2 -0
  127. infrahub/trigger/tasks.py +3 -0
  128. infrahub/webhook/models.py +1 -1
  129. infrahub/workflows/catalogue.py +110 -3
  130. infrahub/workflows/initialization.py +16 -0
  131. infrahub/workflows/models.py +17 -2
  132. infrahub_sdk/branch.py +5 -8
  133. infrahub_sdk/checks.py +1 -1
  134. infrahub_sdk/client.py +364 -84
  135. infrahub_sdk/convert_object_type.py +61 -0
  136. infrahub_sdk/ctl/check.py +2 -3
  137. infrahub_sdk/ctl/cli_commands.py +18 -12
  138. infrahub_sdk/ctl/config.py +8 -2
  139. infrahub_sdk/ctl/generator.py +6 -3
  140. infrahub_sdk/ctl/graphql.py +184 -0
  141. infrahub_sdk/ctl/repository.py +39 -1
  142. infrahub_sdk/ctl/schema.py +18 -3
  143. infrahub_sdk/ctl/utils.py +4 -0
  144. infrahub_sdk/ctl/validate.py +5 -3
  145. infrahub_sdk/diff.py +4 -5
  146. infrahub_sdk/exceptions.py +2 -0
  147. infrahub_sdk/generator.py +7 -1
  148. infrahub_sdk/graphql/__init__.py +12 -0
  149. infrahub_sdk/graphql/constants.py +1 -0
  150. infrahub_sdk/graphql/plugin.py +85 -0
  151. infrahub_sdk/graphql/query.py +77 -0
  152. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  153. infrahub_sdk/graphql/utils.py +40 -0
  154. infrahub_sdk/node/attribute.py +2 -0
  155. infrahub_sdk/node/node.py +28 -20
  156. infrahub_sdk/playback.py +1 -2
  157. infrahub_sdk/protocols.py +54 -6
  158. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  159. infrahub_sdk/pytest_plugin/utils.py +40 -0
  160. infrahub_sdk/repository.py +1 -2
  161. infrahub_sdk/schema/__init__.py +38 -0
  162. infrahub_sdk/schema/main.py +1 -0
  163. infrahub_sdk/schema/repository.py +8 -0
  164. infrahub_sdk/spec/object.py +120 -7
  165. infrahub_sdk/spec/range_expansion.py +118 -0
  166. infrahub_sdk/timestamp.py +18 -6
  167. infrahub_sdk/transforms.py +1 -1
  168. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +9 -11
  169. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +177 -134
  170. infrahub_testcontainers/container.py +1 -1
  171. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  172. infrahub_testcontainers/docker-compose.test.yml +1 -1
  173. infrahub_testcontainers/models.py +2 -2
  174. infrahub_testcontainers/performance_test.py +4 -4
  175. infrahub/core/convert_object_type/conversion.py +0 -134
  176. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
  177. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
  178. {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
- VARIABLE_TYPE_MAPPING = ((str, "String!"), (int, "Int!"), (float, "Float!"), (bool, "Boolean!"))
9
+ from .constants import VARIABLE_TYPE_MAPPING
9
10
 
10
11
 
11
- def convert_to_graphql_as_string(value: str | bool | list | BaseModel | Enum | Any, convert_enum: bool = False) -> str: # noqa: PLR0911
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
- return f'"{value}"'
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
@@ -76,6 +76,8 @@ class Attribute:
76
76
  variables: dict[str, Any] = {}
77
77
 
78
78
  if self.value is None:
79
+ if self._schema.optional and self.value_has_been_mutated:
80
+ data["value"] = None
79
81
  return data
80
82
 
81
83
  if isinstance(self.value, str):
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
- # BLOCKED by https://github.com/opsmill/infrahub/issues/330
238
- # if (
239
- # item is None
240
- # and item_name in self._relationships
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
- if not data_item and data_item != [] and item in data:
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
- content = await self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
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
- if rel_schema and prefetch_relationships:
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, node_data: dict[str, Any], branch: str, related_nodes: list[InfrahubNode], timeout: int | None = None
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
- content = self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
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
- if rel_schema and prefetch_relationships:
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
- response = httpx.Response(status_code=data["status_code"], content=data["response_content"], request=request)
60
- return response
59
+ return httpx.Response(status_code=data["status_code"], content=data["response_content"], request=request)