infrahub-server 1.4.13__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/actions/tasks.py +208 -16
- infrahub/api/artifact.py +3 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/api/internal.py +2 -0
- infrahub/api/query.py +2 -0
- infrahub/api/schema.py +27 -3
- infrahub/auth.py +5 -5
- infrahub/cli/__init__.py +2 -0
- infrahub/cli/db.py +160 -157
- infrahub/cli/dev.py +118 -0
- infrahub/cli/upgrade.py +56 -9
- infrahub/computed_attribute/tasks.py +19 -7
- infrahub/config.py +7 -2
- infrahub/core/attribute.py +35 -24
- infrahub/core/branch/enums.py +1 -1
- infrahub/core/branch/models.py +9 -5
- infrahub/core/branch/needs_rebase_status.py +11 -0
- infrahub/core/branch/tasks.py +72 -10
- infrahub/core/changelog/models.py +2 -10
- infrahub/core/constants/__init__.py +4 -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 -0
- infrahub/core/diff/query/field_summary.py +1 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +7 -4
- infrahub/core/manager.py +3 -81
- infrahub/core/migrations/__init__.py +3 -0
- infrahub/core/migrations/exceptions.py +4 -0
- infrahub/core/migrations/graph/__init__.py +11 -10
- infrahub/core/migrations/graph/load_schema_branch.py +21 -0
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
- infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
- infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
- infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
- infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
- infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
- infrahub/core/migrations/query/__init__.py +7 -8
- infrahub/core/migrations/query/attribute_add.py +8 -6
- infrahub/core/migrations/query/attribute_remove.py +134 -0
- infrahub/core/migrations/runner.py +54 -0
- infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
- infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
- infrahub/core/migrations/schema/node_attribute_add.py +26 -5
- infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
- infrahub/core/migrations/schema/node_kind_update.py +2 -1
- infrahub/core/migrations/schema/node_remove.py +2 -1
- infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
- infrahub/core/migrations/shared.py +66 -19
- infrahub/core/models.py +2 -2
- infrahub/core/node/__init__.py +207 -54
- infrahub/core/node/create.py +53 -49
- infrahub/core/node/lock_utils.py +124 -0
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
- infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
- infrahub/core/node/resource_manager/number_pool.py +2 -1
- infrahub/core/node/standard.py +1 -1
- infrahub/core/property.py +11 -0
- infrahub/core/protocols.py +8 -1
- infrahub/core/query/attribute.py +82 -15
- infrahub/core/query/ipam.py +16 -4
- infrahub/core/query/node.py +66 -188
- infrahub/core/query/relationship.py +44 -26
- infrahub/core/query/subquery.py +0 -8
- infrahub/core/relationship/model.py +69 -24
- infrahub/core/schema/__init__.py +56 -0
- infrahub/core/schema/attribute_schema.py +4 -2
- infrahub/core/schema/basenode_schema.py +42 -2
- infrahub/core/schema/definitions/core/__init__.py +2 -0
- infrahub/core/schema/definitions/core/check.py +1 -1
- 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/core/transform.py +1 -1
- infrahub/core/schema/definitions/internal.py +12 -3
- infrahub/core/schema/generated/attribute_schema.py +2 -2
- infrahub/core/schema/generated/base_node_schema.py +6 -1
- infrahub/core/schema/manager.py +3 -0
- infrahub/core/schema/node_schema.py +1 -0
- infrahub/core/schema/relationship_schema.py +0 -1
- infrahub/core/schema/schema_branch.py +295 -10
- infrahub/core/schema/schema_branch_display.py +135 -0
- infrahub/core/schema/schema_branch_hfid.py +120 -0
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/database/graph.py +21 -0
- 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 +192 -0
- infrahub/display_labels/triggers.py +22 -0
- infrahub/events/branch_action.py +27 -1
- 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/api/dependencies.py +2 -4
- infrahub/graphql/api/endpoints.py +16 -6
- infrahub/graphql/app.py +2 -4
- infrahub/graphql/initialization.py +2 -3
- infrahub/graphql/manager.py +213 -137
- infrahub/graphql/middleware.py +12 -0
- infrahub/graphql/mutations/branch.py +16 -0
- infrahub/graphql/mutations/computed_attribute.py +110 -3
- infrahub/graphql/mutations/convert_object_type.py +44 -13
- infrahub/graphql/mutations/display_label.py +118 -0
- infrahub/graphql/mutations/generator.py +25 -7
- infrahub/graphql/mutations/hfid.py +125 -0
- infrahub/graphql/mutations/ipam.py +73 -41
- infrahub/graphql/mutations/main.py +61 -178
- infrahub/graphql/mutations/profile.py +195 -0
- infrahub/graphql/mutations/proposed_change.py +8 -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/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/graphql/schema_sort.py +170 -0
- infrahub/graphql/types/branch.py +4 -1
- infrahub/graphql/types/enums.py +3 -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 +191 -0
- infrahub/hfid/triggers.py +22 -0
- infrahub/lock.py +119 -42
- infrahub/locks/__init__.py +0 -0
- infrahub/locks/tasks.py +37 -0
- infrahub/patch/plan_writer.py +2 -2
- infrahub/permissions/constants.py +2 -0
- 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 +98 -0
- infrahub/profiles/tasks.py +63 -0
- infrahub/proposed_change/tasks.py +24 -5
- infrahub/repositories/__init__.py +0 -0
- infrahub/repositories/create_repository.py +113 -0
- infrahub/server.py +9 -1
- infrahub/services/__init__.py +8 -5
- infrahub/services/adapters/workflow/worker.py +5 -2
- infrahub/task_manager/event.py +5 -0
- infrahub/task_manager/models.py +7 -0
- infrahub/tasks/registry.py +6 -4
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/setup.py +13 -4
- infrahub/trigger/tasks.py +6 -0
- infrahub/webhook/models.py +1 -1
- infrahub/workers/dependencies.py +3 -1
- infrahub/workers/infrahub_async.py +5 -1
- infrahub/workflows/catalogue.py +118 -3
- infrahub/workflows/initialization.py +21 -0
- infrahub/workflows/models.py +17 -2
- infrahub_sdk/branch.py +17 -8
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/client.py +376 -95
- infrahub_sdk/config.py +29 -2
- infrahub_sdk/convert_object_type.py +61 -0
- infrahub_sdk/ctl/branch.py +3 -0
- infrahub_sdk/ctl/check.py +2 -3
- infrahub_sdk/ctl/cli_commands.py +20 -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 +40 -10
- infrahub_sdk/ctl/task.py +110 -0
- 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/node/relationship.py +1 -3
- 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 +70 -4
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/models.py +7 -0
- infrahub_sdk/spec/object.py +54 -6
- infrahub_sdk/spec/processors/__init__.py +0 -0
- infrahub_sdk/spec/processors/data_processor.py +10 -0
- infrahub_sdk/spec/processors/factory.py +34 -0
- infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
- infrahub_sdk/spec/range_expansion.py +118 -0
- infrahub_sdk/task/models.py +6 -4
- infrahub_sdk/timestamp.py +18 -6
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
- {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +221 -165
- infrahub_testcontainers/container.py +114 -2
- infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
- infrahub_testcontainers/docker-compose.test.yml +5 -0
- 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.13.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
infrahub_sdk/ctl/task.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..async_typer import AsyncTyper
|
|
10
|
+
from ..task.manager import TaskFilter
|
|
11
|
+
from ..task.models import Task, TaskState
|
|
12
|
+
from .client import initialize_client
|
|
13
|
+
from .parameters import CONFIG_PARAM
|
|
14
|
+
from .utils import catch_exception, init_logging
|
|
15
|
+
|
|
16
|
+
app = AsyncTyper()
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.callback()
|
|
21
|
+
def callback() -> None:
|
|
22
|
+
"""Manage Infrahub tasks."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_states(states: list[str] | None) -> list[TaskState] | None:
|
|
26
|
+
if not states:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
parsed_states: list[TaskState] = []
|
|
30
|
+
for state in states:
|
|
31
|
+
normalized_state = state.strip().upper()
|
|
32
|
+
try:
|
|
33
|
+
parsed_states.append(TaskState(normalized_state))
|
|
34
|
+
except ValueError as exc: # pragma: no cover - typer will surface this as CLI error
|
|
35
|
+
raise typer.BadParameter(
|
|
36
|
+
f"Unsupported state '{state}'. Available states: {', '.join(item.value.lower() for item in TaskState)}"
|
|
37
|
+
) from exc
|
|
38
|
+
|
|
39
|
+
return parsed_states
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _render_table(tasks: list[Task]) -> None:
|
|
43
|
+
table = Table(title="Infrahub Tasks", box=None)
|
|
44
|
+
table.add_column("ID", style="cyan", overflow="fold")
|
|
45
|
+
table.add_column("Title", style="magenta", overflow="fold")
|
|
46
|
+
table.add_column("State", style="green")
|
|
47
|
+
table.add_column("Progress", justify="right")
|
|
48
|
+
table.add_column("Workflow", overflow="fold")
|
|
49
|
+
table.add_column("Branch", overflow="fold")
|
|
50
|
+
table.add_column("Updated")
|
|
51
|
+
|
|
52
|
+
if not tasks:
|
|
53
|
+
table.add_row("-", "No tasks found", "-", "-", "-", "-", "-")
|
|
54
|
+
console.print(table)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
for task in tasks:
|
|
58
|
+
progress = f"{task.progress:.0%}" if task.progress is not None else "-"
|
|
59
|
+
table.add_row(
|
|
60
|
+
task.id,
|
|
61
|
+
task.title,
|
|
62
|
+
task.state.value,
|
|
63
|
+
progress,
|
|
64
|
+
task.workflow or "-",
|
|
65
|
+
task.branch or "-",
|
|
66
|
+
task.updated_at.isoformat(),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
console.print(table)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command(name="list")
|
|
73
|
+
@catch_exception(console=console)
|
|
74
|
+
async def list_tasks(
|
|
75
|
+
state: list[str] = typer.Option(
|
|
76
|
+
None, "--state", "-s", help="Filter by task state. Can be provided multiple times."
|
|
77
|
+
),
|
|
78
|
+
limit: Optional[int] = typer.Option(None, help="Maximum number of tasks to retrieve."),
|
|
79
|
+
offset: Optional[int] = typer.Option(None, help="Offset for pagination."),
|
|
80
|
+
include_related_nodes: bool = typer.Option(False, help="Include related nodes in the output."),
|
|
81
|
+
include_logs: bool = typer.Option(False, help="Include task logs in the output."),
|
|
82
|
+
json_output: bool = typer.Option(False, "--json", help="Output the result as JSON."),
|
|
83
|
+
debug: bool = False,
|
|
84
|
+
_: str = CONFIG_PARAM,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""List Infrahub tasks."""
|
|
87
|
+
|
|
88
|
+
init_logging(debug=debug)
|
|
89
|
+
|
|
90
|
+
client = initialize_client()
|
|
91
|
+
filters = TaskFilter()
|
|
92
|
+
parsed_states = _parse_states(state)
|
|
93
|
+
if parsed_states:
|
|
94
|
+
filters.state = parsed_states
|
|
95
|
+
|
|
96
|
+
tasks = await client.task.filter(
|
|
97
|
+
filter=filters,
|
|
98
|
+
limit=limit,
|
|
99
|
+
offset=offset,
|
|
100
|
+
include_related_nodes=include_related_nodes,
|
|
101
|
+
include_logs=include_logs,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if json_output:
|
|
105
|
+
console.print_json(
|
|
106
|
+
data=[task.model_dump(mode="json") for task in tasks], indent=2, sort_keys=True, highlight=False
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
_render_table(tasks)
|
infrahub_sdk/ctl/utils.py
CHANGED
|
@@ -118,6 +118,10 @@ def execute_graphql_query(
|
|
|
118
118
|
query_str = query_object.load_query()
|
|
119
119
|
|
|
120
120
|
client = initialize_client_sync()
|
|
121
|
+
|
|
122
|
+
if not branch:
|
|
123
|
+
branch = client.config.default_infrahub_branch
|
|
124
|
+
|
|
121
125
|
response = client.execute_graphql(
|
|
122
126
|
query=query_str,
|
|
123
127
|
branch_name=branch,
|
infrahub_sdk/ctl/validate.py
CHANGED
|
@@ -14,7 +14,7 @@ from ..ctl.client import initialize_client, initialize_client_sync
|
|
|
14
14
|
from ..ctl.exceptions import QueryNotFoundError
|
|
15
15
|
from ..ctl.utils import catch_exception, find_graphql_query, parse_cli_vars
|
|
16
16
|
from ..exceptions import GraphQLError
|
|
17
|
-
from ..utils import
|
|
17
|
+
from ..utils import write_to_file
|
|
18
18
|
from ..yaml import SchemaFile
|
|
19
19
|
from .parameters import CONFIG_PARAM
|
|
20
20
|
from .utils import load_yamlfile_from_disk_and_exit
|
|
@@ -68,8 +68,6 @@ def validate_graphql(
|
|
|
68
68
|
) -> None:
|
|
69
69
|
"""Validate the format of a GraphQL Query stored locally by executing it on a remote GraphQL endpoint"""
|
|
70
70
|
|
|
71
|
-
branch = get_branch(branch)
|
|
72
|
-
|
|
73
71
|
try:
|
|
74
72
|
query_str = find_graphql_query(query)
|
|
75
73
|
except QueryNotFoundError:
|
|
@@ -81,6 +79,10 @@ def validate_graphql(
|
|
|
81
79
|
variables_dict = parse_cli_vars(variables)
|
|
82
80
|
|
|
83
81
|
client = initialize_client_sync()
|
|
82
|
+
|
|
83
|
+
if not branch:
|
|
84
|
+
branch = client.config.default_infrahub_branch
|
|
85
|
+
|
|
84
86
|
try:
|
|
85
87
|
response = client.execute_graphql(
|
|
86
88
|
query=query_str,
|
infrahub_sdk/diff.py
CHANGED
|
@@ -37,8 +37,8 @@ class NodeDiffPeer(TypedDict):
|
|
|
37
37
|
|
|
38
38
|
def get_diff_summary_query() -> str:
|
|
39
39
|
return """
|
|
40
|
-
query GetDiffTree($branch_name: String
|
|
41
|
-
DiffTree(branch: $branch_name) {
|
|
40
|
+
query GetDiffTree($branch_name: String!, $name: String, $from_time: DateTime, $to_time: DateTime) {
|
|
41
|
+
DiffTree(branch: $branch_name, name: $name, from_time: $from_time, to_time: $to_time) {
|
|
42
42
|
nodes {
|
|
43
43
|
uuid
|
|
44
44
|
kind
|
|
@@ -117,12 +117,11 @@ def diff_tree_node_to_node_diff(node_dict: dict[str, Any], branch_name: str) ->
|
|
|
117
117
|
)
|
|
118
118
|
relationship_diff["peers"] = peer_diffs
|
|
119
119
|
element_diffs.append(relationship_diff)
|
|
120
|
-
|
|
120
|
+
return NodeDiff(
|
|
121
121
|
branch=branch_name,
|
|
122
122
|
kind=str(node_dict.get("kind")),
|
|
123
123
|
id=str(node_dict.get("uuid")),
|
|
124
|
-
action=str(node_dict.get("
|
|
124
|
+
action=str(node_dict.get("status")),
|
|
125
125
|
display_label=str(node_dict.get("label")),
|
|
126
126
|
elements=element_diffs,
|
|
127
127
|
)
|
|
128
|
-
return node_diff
|
infrahub_sdk/exceptions.py
CHANGED
|
@@ -17,6 +17,8 @@ class JsonDecodeError(Error):
|
|
|
17
17
|
self.url = url
|
|
18
18
|
if not self.message and self.url:
|
|
19
19
|
self.message = f"Unable to decode response as JSON data from {self.url}"
|
|
20
|
+
if self.content:
|
|
21
|
+
self.message += f". Server response: {self.content}"
|
|
20
22
|
super().__init__(self.message)
|
|
21
23
|
|
|
22
24
|
|
infrahub_sdk/generator.py
CHANGED
|
@@ -26,6 +26,8 @@ class InfrahubGenerator(InfrahubOperation):
|
|
|
26
26
|
generator_instance: str = "",
|
|
27
27
|
params: dict | None = None,
|
|
28
28
|
convert_query_response: bool = False,
|
|
29
|
+
execute_in_proposed_change: bool = True,
|
|
30
|
+
execute_after_merge: bool = True,
|
|
29
31
|
logger: logging.Logger | None = None,
|
|
30
32
|
request_context: RequestContext | None = None,
|
|
31
33
|
) -> None:
|
|
@@ -44,6 +46,8 @@ class InfrahubGenerator(InfrahubOperation):
|
|
|
44
46
|
self._client: InfrahubClient | None = None
|
|
45
47
|
self.logger = logger if logger else logging.getLogger("infrahub.tasks")
|
|
46
48
|
self.request_context = request_context
|
|
49
|
+
self.execute_in_proposed_change = execute_in_proposed_change
|
|
50
|
+
self.execute_after_merge = execute_after_merge
|
|
47
51
|
|
|
48
52
|
@property
|
|
49
53
|
def subscribers(self) -> list[str] | None:
|
|
@@ -81,8 +85,10 @@ class InfrahubGenerator(InfrahubOperation):
|
|
|
81
85
|
unpacked = data.get("data") or data
|
|
82
86
|
await self.process_nodes(data=unpacked)
|
|
83
87
|
|
|
88
|
+
group_type = "CoreGeneratorGroup" if self.execute_after_merge else "CoreGeneratorAwareGroup"
|
|
89
|
+
|
|
84
90
|
async with self._init_client.start_tracking(
|
|
85
|
-
identifier=identifier, params=self.params, delete_unused_nodes=True, group_type=
|
|
91
|
+
identifier=identifier, params=self.params, delete_unused_nodes=True, group_type=group_type
|
|
86
92
|
) as self.client:
|
|
87
93
|
await self.generate(data=unpacked)
|
|
88
94
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .constants import VARIABLE_TYPE_MAPPING
|
|
2
|
+
from .query import Mutation, Query
|
|
3
|
+
from .renderers import render_input_block, render_query_block, render_variables_to_string
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"VARIABLE_TYPE_MAPPING",
|
|
7
|
+
"Mutation",
|
|
8
|
+
"Query",
|
|
9
|
+
"render_input_block",
|
|
10
|
+
"render_query_block",
|
|
11
|
+
"render_variables_to_string",
|
|
12
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VARIABLE_TYPE_MAPPING = ((str, "String!"), (int, "Int!"), (float, "Float!"), (bool, "Boolean!"))
|
|
@@ -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
|