infrahub-server 1.5.0b0__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 +8 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/cli/db.py +24 -0
- infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
- infrahub/core/attribute.py +3 -3
- infrahub/core/branch/tasks.py +2 -1
- infrahub/core/changelog/models.py +4 -12
- infrahub/core/constants/infrahubkind.py +1 -0
- infrahub/core/diff/model/path.py +4 -0
- infrahub/core/diff/payload_builder.py +1 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/ipam/utilization.py +1 -1
- infrahub/core/manager.py +6 -3
- infrahub/core/migrations/graph/__init__.py +4 -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 +142 -40
- infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/standard.py +1 -1
- infrahub/core/protocols.py +7 -1
- infrahub/core/query/node.py +14 -1
- infrahub/core/registry.py +2 -2
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/model.py +1 -1
- 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/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/schema_branch.py +134 -0
- 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 +7 -0
- infrahub/generators/tasks.py +31 -15
- infrahub/git/integrator.py +22 -14
- infrahub/graphql/analyzer.py +1 -1
- 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/relationship.py +2 -2
- infrahub/graphql/mutations/resource_manager.py +2 -2
- infrahub/graphql/mutations/schema.py +5 -5
- infrahub/graphql/queries/resource_manager.py +1 -1
- infrahub/graphql/resolvers/resolver.py +2 -0
- infrahub/graphql/schema.py +4 -0
- 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 +15 -4
- infrahub/middleware.py +26 -1
- infrahub/proposed_change/tasks.py +10 -1
- infrahub/server.py +16 -3
- infrahub/services/__init__.py +8 -5
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/tasks.py +3 -0
- infrahub/workflows/catalogue.py +72 -0
- infrahub/workflows/initialization.py +16 -0
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/ctl/cli_commands.py +2 -0
- infrahub_sdk/ctl/generator.py +4 -0
- infrahub_sdk/ctl/graphql.py +184 -0
- infrahub_sdk/ctl/schema.py +6 -2
- 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} +81 -73
- infrahub_sdk/graphql/utils.py +40 -0
- infrahub_sdk/protocols.py +14 -0
- infrahub_sdk/schema/__init__.py +38 -0
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/object.py +84 -10
- infrahub_sdk/spec/range_expansion.py +1 -1
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +5 -4
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +104 -79
- infrahub_testcontainers/container.py +1 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -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"
|
|
@@ -6,10 +6,35 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
from .constants import VARIABLE_TYPE_MAPPING
|
|
10
10
|
|
|
11
11
|
|
|
12
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
|
+
"""
|
|
13
38
|
if value is None:
|
|
14
39
|
return "null"
|
|
15
40
|
if isinstance(value, str) and value.startswith("$"):
|
|
@@ -56,6 +81,34 @@ def render_variables_to_string(data: dict[str, type[str | int | float | bool]])
|
|
|
56
81
|
|
|
57
82
|
|
|
58
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
|
+
"""
|
|
59
112
|
FILTERS_KEY = "@filters"
|
|
60
113
|
ALIAS_KEY = "@alias"
|
|
61
114
|
KEYWORDS_TO_SKIP = [FILTERS_KEY, ALIAS_KEY]
|
|
@@ -97,6 +150,33 @@ def render_query_block(data: dict, offset: int = 4, indentation: int = 4, conver
|
|
|
97
150
|
|
|
98
151
|
|
|
99
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
|
+
"""
|
|
100
180
|
offset_str = " " * offset
|
|
101
181
|
lines = []
|
|
102
182
|
for key, value in data.items():
|
|
@@ -130,75 +210,3 @@ def render_input_block(data: dict, offset: int = 4, indentation: int = 4, conver
|
|
|
130
210
|
else:
|
|
131
211
|
lines.append(f"{offset_str}{key}: {convert_to_graphql_as_string(value=value, convert_enum=convert_enum)}")
|
|
132
212
|
return lines
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
class BaseGraphQLQuery:
|
|
136
|
-
query_type: str = "not-defined"
|
|
137
|
-
indentation: int = 4
|
|
138
|
-
|
|
139
|
-
def __init__(self, query: dict, variables: dict | None = None, name: str | None = None):
|
|
140
|
-
self.query = query
|
|
141
|
-
self.variables = variables
|
|
142
|
-
self.name = name or ""
|
|
143
|
-
|
|
144
|
-
def render_first_line(self) -> str:
|
|
145
|
-
first_line = self.query_type
|
|
146
|
-
|
|
147
|
-
if self.name:
|
|
148
|
-
first_line += " " + self.name
|
|
149
|
-
|
|
150
|
-
if self.variables:
|
|
151
|
-
first_line += f" ({render_variables_to_string(self.variables)})"
|
|
152
|
-
|
|
153
|
-
first_line += " {"
|
|
154
|
-
|
|
155
|
-
return first_line
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
class Query(BaseGraphQLQuery):
|
|
159
|
-
query_type = "query"
|
|
160
|
-
|
|
161
|
-
def render(self, convert_enum: bool = False) -> str:
|
|
162
|
-
lines = [self.render_first_line()]
|
|
163
|
-
lines.extend(
|
|
164
|
-
render_query_block(
|
|
165
|
-
data=self.query, indentation=self.indentation, offset=self.indentation, convert_enum=convert_enum
|
|
166
|
-
)
|
|
167
|
-
)
|
|
168
|
-
lines.append("}")
|
|
169
|
-
|
|
170
|
-
return "\n" + "\n".join(lines) + "\n"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class Mutation(BaseGraphQLQuery):
|
|
174
|
-
query_type = "mutation"
|
|
175
|
-
|
|
176
|
-
def __init__(self, *args: Any, mutation: str, input_data: dict, **kwargs: Any):
|
|
177
|
-
self.input_data = input_data
|
|
178
|
-
self.mutation = mutation
|
|
179
|
-
super().__init__(*args, **kwargs)
|
|
180
|
-
|
|
181
|
-
def render(self, convert_enum: bool = False) -> str:
|
|
182
|
-
lines = [self.render_first_line()]
|
|
183
|
-
lines.append(" " * self.indentation + f"{self.mutation}(")
|
|
184
|
-
lines.extend(
|
|
185
|
-
render_input_block(
|
|
186
|
-
data=self.input_data,
|
|
187
|
-
indentation=self.indentation,
|
|
188
|
-
offset=self.indentation * 2,
|
|
189
|
-
convert_enum=convert_enum,
|
|
190
|
-
)
|
|
191
|
-
)
|
|
192
|
-
lines.append(" " * self.indentation + "){")
|
|
193
|
-
lines.extend(
|
|
194
|
-
render_query_block(
|
|
195
|
-
data=self.query,
|
|
196
|
-
indentation=self.indentation,
|
|
197
|
-
offset=self.indentation * 2,
|
|
198
|
-
convert_enum=convert_enum,
|
|
199
|
-
)
|
|
200
|
-
)
|
|
201
|
-
lines.append(" " * self.indentation + "}")
|
|
202
|
-
lines.append("}")
|
|
203
|
-
|
|
204
|
-
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/protocols.py
CHANGED
|
@@ -131,6 +131,7 @@ class CoreGenericRepository(CoreNode):
|
|
|
131
131
|
queries: RelationshipManager
|
|
132
132
|
checks: RelationshipManager
|
|
133
133
|
generators: RelationshipManager
|
|
134
|
+
groups_objects: RelationshipManager
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
class CoreGroup(CoreNode):
|
|
@@ -355,6 +356,10 @@ class CoreGeneratorAction(CoreAction):
|
|
|
355
356
|
generator: RelatedNode
|
|
356
357
|
|
|
357
358
|
|
|
359
|
+
class CoreGeneratorAwareGroup(CoreGroup):
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
|
|
358
363
|
class CoreGeneratorCheck(CoreCheck):
|
|
359
364
|
instance: String
|
|
360
365
|
|
|
@@ -366,6 +371,8 @@ class CoreGeneratorDefinition(CoreTaskTarget):
|
|
|
366
371
|
file_path: String
|
|
367
372
|
class_name: String
|
|
368
373
|
convert_query_response: BooleanOptional
|
|
374
|
+
execute_in_proposed_change: BooleanOptional
|
|
375
|
+
execute_after_merge: BooleanOptional
|
|
369
376
|
query: RelatedNode
|
|
370
377
|
repository: RelatedNode
|
|
371
378
|
targets: RelatedNode
|
|
@@ -681,6 +688,7 @@ class CoreGenericRepositorySync(CoreNodeSync):
|
|
|
681
688
|
queries: RelationshipManagerSync
|
|
682
689
|
checks: RelationshipManagerSync
|
|
683
690
|
generators: RelationshipManagerSync
|
|
691
|
+
groups_objects: RelationshipManagerSync
|
|
684
692
|
|
|
685
693
|
|
|
686
694
|
class CoreGroupSync(CoreNodeSync):
|
|
@@ -905,6 +913,10 @@ class CoreGeneratorActionSync(CoreActionSync):
|
|
|
905
913
|
generator: RelatedNodeSync
|
|
906
914
|
|
|
907
915
|
|
|
916
|
+
class CoreGeneratorAwareGroupSync(CoreGroupSync):
|
|
917
|
+
pass
|
|
918
|
+
|
|
919
|
+
|
|
908
920
|
class CoreGeneratorCheckSync(CoreCheckSync):
|
|
909
921
|
instance: String
|
|
910
922
|
|
|
@@ -916,6 +928,8 @@ class CoreGeneratorDefinitionSync(CoreTaskTargetSync):
|
|
|
916
928
|
file_path: String
|
|
917
929
|
class_name: String
|
|
918
930
|
convert_query_response: BooleanOptional
|
|
931
|
+
execute_in_proposed_change: BooleanOptional
|
|
932
|
+
execute_after_merge: BooleanOptional
|
|
919
933
|
query: RelatedNodeSync
|
|
920
934
|
repository: RelatedNodeSync
|
|
921
935
|
targets: RelatedNodeSync
|
infrahub_sdk/schema/__init__.py
CHANGED
|
@@ -474,6 +474,25 @@ class InfrahubSchema(InfrahubSchemaBase):
|
|
|
474
474
|
|
|
475
475
|
return branch_schema.nodes
|
|
476
476
|
|
|
477
|
+
async def get_graphql_schema(self, branch: str | None = None) -> str:
|
|
478
|
+
"""Get the GraphQL schema as a string.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
branch: The branch to get the schema for. Defaults to default_branch.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
The GraphQL schema as a string.
|
|
485
|
+
"""
|
|
486
|
+
branch = branch or self.client.default_branch
|
|
487
|
+
url = f"{self.client.address}/schema.graphql?branch={branch}"
|
|
488
|
+
|
|
489
|
+
response = await self.client._get(url=url)
|
|
490
|
+
|
|
491
|
+
if response.status_code != 200:
|
|
492
|
+
raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")
|
|
493
|
+
|
|
494
|
+
return response.text
|
|
495
|
+
|
|
477
496
|
async def _fetch(self, branch: str, namespaces: list[str] | None = None) -> BranchSchema:
|
|
478
497
|
url_parts = [("branch", branch)]
|
|
479
498
|
if namespaces:
|
|
@@ -697,6 +716,25 @@ class InfrahubSchemaSync(InfrahubSchemaBase):
|
|
|
697
716
|
|
|
698
717
|
return branch_schema.nodes
|
|
699
718
|
|
|
719
|
+
def get_graphql_schema(self, branch: str | None = None) -> str:
|
|
720
|
+
"""Get the GraphQL schema as a string.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
branch: The branch to get the schema for. Defaults to default_branch.
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
The GraphQL schema as a string.
|
|
727
|
+
"""
|
|
728
|
+
branch = branch or self.client.default_branch
|
|
729
|
+
url = f"{self.client.address}/schema.graphql?branch={branch}"
|
|
730
|
+
|
|
731
|
+
response = self.client._get(url=url)
|
|
732
|
+
|
|
733
|
+
if response.status_code != 200:
|
|
734
|
+
raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")
|
|
735
|
+
|
|
736
|
+
return response.text
|
|
737
|
+
|
|
700
738
|
def _fetch(self, branch: str, namespaces: list[str] | None = None) -> BranchSchema:
|
|
701
739
|
url_parts = [("branch", branch)]
|
|
702
740
|
if namespaces:
|
|
@@ -96,6 +96,14 @@ class InfrahubGeneratorDefinitionConfig(InfrahubRepositoryConfigElement):
|
|
|
96
96
|
default=False,
|
|
97
97
|
description="Decide if the generator should convert the result of the GraphQL query to SDK InfrahubNode objects.",
|
|
98
98
|
)
|
|
99
|
+
execute_in_proposed_change: bool = Field(
|
|
100
|
+
default=True,
|
|
101
|
+
description="Decide if the generator should execute in a proposed change.",
|
|
102
|
+
)
|
|
103
|
+
execute_after_merge: bool = Field(
|
|
104
|
+
default=True,
|
|
105
|
+
description="Decide if the generator should execute after a merge.",
|
|
106
|
+
)
|
|
99
107
|
|
|
100
108
|
def load_class(self, import_root: str | None = None, relative_path: str | None = None) -> type[InfrahubGenerator]:
|
|
101
109
|
module = import_module(module_path=self.file_path, import_root=import_root, relative_path=relative_path)
|
infrahub_sdk/spec/object.py
CHANGED
|
@@ -2,8 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import re
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
5
6
|
from enum import Enum
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
7
8
|
|
|
8
9
|
from pydantic import BaseModel, Field
|
|
9
10
|
|
|
@@ -45,6 +46,11 @@ class RelationshipDataFormat(str, Enum):
|
|
|
45
46
|
MANY_REF = "many_ref_list"
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
class ObjectStrategy(str, Enum):
|
|
50
|
+
NORMAL = "normal"
|
|
51
|
+
RANGE_EXPAND = "range_expand"
|
|
52
|
+
|
|
53
|
+
|
|
48
54
|
class RelationshipInfo(BaseModel):
|
|
49
55
|
name: str
|
|
50
56
|
rel_schema: RelationshipSchema
|
|
@@ -168,7 +174,7 @@ async def get_relationship_info(
|
|
|
168
174
|
|
|
169
175
|
|
|
170
176
|
def expand_data_with_ranges(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
171
|
-
"""Expand any item in
|
|
177
|
+
"""Expand any item in data with range pattern in any value. Supports multiple fields, requires equal expansion length."""
|
|
172
178
|
range_pattern = re.compile(MATCH_PATTERN)
|
|
173
179
|
expanded = []
|
|
174
180
|
for item in data:
|
|
@@ -198,16 +204,69 @@ def expand_data_with_ranges(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
198
204
|
return expanded
|
|
199
205
|
|
|
200
206
|
|
|
207
|
+
class DataProcessor(ABC):
|
|
208
|
+
"""Abstract base class for data processing strategies"""
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
212
|
+
"""Process the data according to the strategy"""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class SingleDataProcessor(DataProcessor):
|
|
216
|
+
"""Process data without any expansion"""
|
|
217
|
+
|
|
218
|
+
def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
219
|
+
return data
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class RangeExpandDataProcessor(DataProcessor):
|
|
223
|
+
"""Process data with range expansion"""
|
|
224
|
+
|
|
225
|
+
def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
226
|
+
return expand_data_with_ranges(data)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class DataProcessorFactory:
|
|
230
|
+
"""Factory to create appropriate data processor based on strategy"""
|
|
231
|
+
|
|
232
|
+
_processors: ClassVar[dict[ObjectStrategy, type[DataProcessor]]] = {
|
|
233
|
+
ObjectStrategy.NORMAL: SingleDataProcessor,
|
|
234
|
+
ObjectStrategy.RANGE_EXPAND: RangeExpandDataProcessor,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def get_processor(cls, strategy: ObjectStrategy) -> DataProcessor:
|
|
239
|
+
processor_class = cls._processors.get(strategy)
|
|
240
|
+
if not processor_class:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Unknown strategy: {strategy} - no processor found. Valid strategies are: {list(cls._processors.keys())}"
|
|
243
|
+
)
|
|
244
|
+
return processor_class()
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def register_processor(cls, strategy: ObjectStrategy, processor_class: type[DataProcessor]) -> None:
|
|
248
|
+
"""Register a new processor for a strategy - useful for future extensions"""
|
|
249
|
+
cls._processors[strategy] = processor_class
|
|
250
|
+
|
|
251
|
+
|
|
201
252
|
class InfrahubObjectFileData(BaseModel):
|
|
202
253
|
kind: str
|
|
254
|
+
strategy: ObjectStrategy = ObjectStrategy.NORMAL
|
|
203
255
|
data: list[dict[str, Any]] = Field(default_factory=list)
|
|
204
256
|
|
|
257
|
+
def _get_processed_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
258
|
+
"""Get data processed according to the strategy"""
|
|
259
|
+
processor = DataProcessorFactory.get_processor(self.strategy)
|
|
260
|
+
return processor.process_data(data)
|
|
261
|
+
|
|
205
262
|
async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ObjectValidationError]:
|
|
206
263
|
errors: list[ObjectValidationError] = []
|
|
207
264
|
schema = await client.schema.get(kind=self.kind, branch=branch)
|
|
208
|
-
|
|
209
|
-
self.data
|
|
210
|
-
|
|
265
|
+
|
|
266
|
+
processed_data = self._get_processed_data(data=self.data)
|
|
267
|
+
self.data = processed_data
|
|
268
|
+
|
|
269
|
+
for idx, item in enumerate(processed_data):
|
|
211
270
|
errors.extend(
|
|
212
271
|
await self.validate_object(
|
|
213
272
|
client=client,
|
|
@@ -216,14 +275,16 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
216
275
|
data=item,
|
|
217
276
|
branch=branch,
|
|
218
277
|
default_schema_kind=self.kind,
|
|
278
|
+
strategy=self.strategy, # Pass strategy down
|
|
219
279
|
)
|
|
220
280
|
)
|
|
221
281
|
return errors
|
|
222
282
|
|
|
223
283
|
async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
|
|
224
284
|
schema = await client.schema.get(kind=self.kind, branch=branch)
|
|
225
|
-
|
|
226
|
-
|
|
285
|
+
processed_data = self._get_processed_data(data=self.data)
|
|
286
|
+
|
|
287
|
+
for idx, item in enumerate(processed_data):
|
|
227
288
|
await self.create_node(
|
|
228
289
|
client=client,
|
|
229
290
|
schema=schema,
|
|
@@ -243,6 +304,7 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
243
304
|
context: dict | None = None,
|
|
244
305
|
branch: str | None = None,
|
|
245
306
|
default_schema_kind: str | None = None,
|
|
307
|
+
strategy: ObjectStrategy = ObjectStrategy.NORMAL,
|
|
246
308
|
) -> list[ObjectValidationError]:
|
|
247
309
|
errors: list[ObjectValidationError] = []
|
|
248
310
|
context = context.copy() if context else {}
|
|
@@ -292,6 +354,7 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
292
354
|
context=context,
|
|
293
355
|
branch=branch,
|
|
294
356
|
default_schema_kind=default_schema_kind,
|
|
357
|
+
strategy=strategy,
|
|
295
358
|
)
|
|
296
359
|
)
|
|
297
360
|
|
|
@@ -307,6 +370,7 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
307
370
|
context: dict | None = None,
|
|
308
371
|
branch: str | None = None,
|
|
309
372
|
default_schema_kind: str | None = None,
|
|
373
|
+
strategy: ObjectStrategy = ObjectStrategy.NORMAL,
|
|
310
374
|
) -> list[ObjectValidationError]:
|
|
311
375
|
context = context.copy() if context else {}
|
|
312
376
|
errors: list[ObjectValidationError] = []
|
|
@@ -348,7 +412,10 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
348
412
|
rel_info.find_matching_relationship(peer_schema=peer_schema)
|
|
349
413
|
context.update(rel_info.get_context(value="placeholder"))
|
|
350
414
|
|
|
351
|
-
|
|
415
|
+
# Use strategy-aware data processing
|
|
416
|
+
processor = DataProcessorFactory.get_processor(strategy)
|
|
417
|
+
expanded_data = processor.process_data(data["data"])
|
|
418
|
+
|
|
352
419
|
for idx, peer_data in enumerate(expanded_data):
|
|
353
420
|
context["list_index"] = idx
|
|
354
421
|
errors.extend(
|
|
@@ -360,6 +427,7 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
360
427
|
context=context,
|
|
361
428
|
branch=branch,
|
|
362
429
|
default_schema_kind=default_schema_kind,
|
|
430
|
+
strategy=strategy,
|
|
363
431
|
)
|
|
364
432
|
)
|
|
365
433
|
return errors
|
|
@@ -633,14 +701,20 @@ class ObjectFile(InfrahubFile):
|
|
|
633
701
|
@property
|
|
634
702
|
def spec(self) -> InfrahubObjectFileData:
|
|
635
703
|
if not self._spec:
|
|
636
|
-
|
|
704
|
+
try:
|
|
705
|
+
self._spec = InfrahubObjectFileData(**self.data.spec)
|
|
706
|
+
except Exception as exc:
|
|
707
|
+
raise ValidationError(identifier=str(self.location), message=str(exc))
|
|
637
708
|
return self._spec
|
|
638
709
|
|
|
639
710
|
def validate_content(self) -> None:
|
|
640
711
|
super().validate_content()
|
|
641
712
|
if self.kind != InfrahubFileKind.OBJECT:
|
|
642
713
|
raise ValueError("File is not an Infrahub Object file")
|
|
643
|
-
|
|
714
|
+
try:
|
|
715
|
+
self._spec = InfrahubObjectFileData(**self.data.spec)
|
|
716
|
+
except Exception as exc:
|
|
717
|
+
raise ValidationError(identifier=str(self.location), message=str(exc))
|
|
644
718
|
|
|
645
719
|
async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> None:
|
|
646
720
|
self.validate_content()
|
infrahub_sdk/transforms.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: infrahub-server
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.0b1
|
|
4
4
|
Summary: Infrahub is taking a new approach to Infrastructure Management by providing a new generation of datastore to organize and control all the data that defines how an infrastructure should run.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: OpsMill
|
|
@@ -13,8 +13,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
13
13
|
Requires-Dist: Jinja2 (>=3,<4)
|
|
14
14
|
Requires-Dist: aio-pika (>=9.4,<9.5)
|
|
15
15
|
Requires-Dist: aiodataloader (==0.4.0)
|
|
16
|
+
Requires-Dist: ariadne-codegen (==0.15.3)
|
|
16
17
|
Requires-Dist: asgi-correlation-id (==4.2.0)
|
|
17
|
-
Requires-Dist: authlib (==1.
|
|
18
|
+
Requires-Dist: authlib (==1.6.5)
|
|
18
19
|
Requires-Dist: bcrypt (>=4.1,<4.2)
|
|
19
20
|
Requires-Dist: boto3 (==1.34.129)
|
|
20
21
|
Requires-Dist: copier (>=9.8.0,<10.0.0)
|
|
@@ -37,8 +38,8 @@ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (==1.28.1)
|
|
|
37
38
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.28.1)
|
|
38
39
|
Requires-Dist: opentelemetry-instrumentation-aio-pika (==0.49b1)
|
|
39
40
|
Requires-Dist: opentelemetry-instrumentation-fastapi (==0.49b1)
|
|
40
|
-
Requires-Dist: prefect (==3.4.
|
|
41
|
-
Requires-Dist: prefect-redis (==0.2.
|
|
41
|
+
Requires-Dist: prefect (==3.4.23)
|
|
42
|
+
Requires-Dist: prefect-redis (==0.2.5)
|
|
42
43
|
Requires-Dist: pyarrow (>=14)
|
|
43
44
|
Requires-Dist: pydantic (>=2.10,<2.11)
|
|
44
45
|
Requires-Dist: pydantic-settings (>=2.8,<2.9)
|