-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathgraphql.py
More file actions
194 lines (155 loc) · 7.07 KB
/
graphql.py
File metadata and controls
194 lines (155 loc) · 7.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
from __future__ import annotations
import ast
from collections import defaultdict
from pathlib import Path
import typer
from ariadne_codegen.client_generators.package import PackageGenerator, get_package_generator
from ariadne_codegen.exceptions import ParsingError
from ariadne_codegen.plugins.explorer import get_plugins_types
from ariadne_codegen.plugins.manager import PluginManager
from ariadne_codegen.schema import (
filter_fragments_definitions,
filter_operations_definitions,
get_graphql_schema_from_path,
)
from ariadne_codegen.settings import ClientSettings, CommentsStrategy
from ariadne_codegen.utils import ast_to_str
from graphql import DefinitionNode, GraphQLSchema, NoUnusedFragmentsRule, parse, specified_rules, validate
from rich.console import Console
from ..async_typer import AsyncTyper
from ..ctl.client import initialize_client
from ..ctl.utils import catch_exception
from ..graphql.utils import (
insert_fragments_inline,
remove_fragment_import,
strip_typename_from_fragment,
strip_typename_from_operation,
)
from .parameters import CONFIG_PARAM
app = AsyncTyper()
console = Console()
ARIADNE_PLUGINS = [
"infrahub_sdk.graphql.plugin.PydanticBaseModelPlugin",
"infrahub_sdk.graphql.plugin.FutureAnnotationPlugin",
"infrahub_sdk.graphql.plugin.StandardTypeHintPlugin",
]
def find_gql_files(query_path: Path) -> list[Path]:
"""
Find all files with .gql extension in the specified directory.
Args:
query_path: Path to the directory to search for .gql files
Returns:
List of Path objects for all .gql files found
"""
if not query_path.exists():
raise FileNotFoundError(f"File or directory not found: {query_path}")
if not query_path.is_dir() and query_path.is_file():
return [query_path]
return list(query_path.glob("**/*.gql"))
def get_graphql_query(queries_path: Path, schema: GraphQLSchema) -> tuple[DefinitionNode, ...]:
"""Get GraphQL queries definitions from a single GraphQL file."""
if not queries_path.exists():
raise FileNotFoundError(f"File not found: {queries_path}")
if not queries_path.is_file():
raise ValueError(f"{queries_path} is not a file")
queries_str = queries_path.read_text(encoding="utf-8")
queries_ast = parse(queries_str)
validation_errors = validate(
schema=schema,
document_ast=queries_ast,
rules=[r for r in specified_rules if r is not NoUnusedFragmentsRule],
)
if validation_errors:
raise ValueError("\n\n".join(error.message for error in validation_errors))
return queries_ast.definitions
def generate_result_types(directory: Path, package: PackageGenerator, fragment: ast.Module) -> None:
for file_name, module in package._result_types_files.items():
file_path = directory / file_name
insert_fragments_inline(module, fragment)
remove_fragment_import(module)
code = package._add_comments_to_code(ast_to_str(module), package.queries_source)
if package.plugin_manager:
code = package.plugin_manager.generate_result_types_code(code)
file_path.write_text(code)
package._generated_files.append(file_path.name)
@app.callback()
def callback() -> None:
"""
Various GraphQL related commands.
"""
@app.command()
@catch_exception(console=console)
async def export_schema(
destination: Path = typer.Option("schema.graphql", help="Path to the GraphQL schema file."),
_: str = CONFIG_PARAM,
) -> None:
"""Export the GraphQL schema to a file."""
client = initialize_client()
schema_text = await client.schema.get_graphql_schema()
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(schema_text, encoding="utf-8")
console.print(f"[green]Schema exported to {destination}")
@app.command()
@catch_exception(console=console)
async def generate_return_types(
query: Path | None = typer.Argument(
None, help="Location of the GraphQL query file(s). Defaults to current directory if not specified."
),
schema: Path = typer.Option("schema.graphql", help="Path to the GraphQL schema file."),
_: str = CONFIG_PARAM,
) -> None:
"""Create Pydantic Models for GraphQL query return types"""
query = Path.cwd() if query is None else query
# Load the GraphQL schema
if not schema.exists():
raise FileNotFoundError(f"GraphQL Schema file not found: {schema}")
graphql_schema = get_graphql_schema_from_path(schema_path=str(schema))
# Initialize the plugin manager
plugin_manager = PluginManager(
schema=graphql_schema,
plugins_types=get_plugins_types(plugins_strs=ARIADNE_PLUGINS),
)
# Find the GraphQL files and organize them by directory
gql_files = find_gql_files(query)
gql_per_directory: dict[Path, list[Path]] = defaultdict(list)
for gql_file in gql_files:
gql_per_directory[gql_file.parent].append(gql_file)
# Generate the Pydantic Models for the GraphQL queries
for directory, gql_files in gql_per_directory.items():
for gql_file in gql_files:
try:
definitions = get_graphql_query(queries_path=gql_file, schema=graphql_schema)
except ValueError as exc:
console.print(f"[red]Error generating result types for {gql_file}: {exc}")
continue
queries = filter_operations_definitions(definitions)
fragments = filter_fragments_definitions(definitions)
# Strip __typename fields from operations and fragments before code generation.
# __typename is a GraphQL introspection meta-field that isn't part of the schema's
# type definitions, causing ariadne-codegen to fail with "Redefinition of reserved type 'String'"
stripped_queries = [strip_typename_from_operation(q) for q in queries]
stripped_fragments = [strip_typename_from_fragment(f) for f in fragments]
package_generator = get_package_generator(
schema=graphql_schema,
fragments=stripped_fragments,
settings=ClientSettings(
schema_path=str(schema),
target_package_name=directory.name or "graphql_client",
queries_path=str(directory),
include_comments=CommentsStrategy.NONE,
),
plugin_manager=plugin_manager,
)
parsing_failed = False
try:
for query_operation in stripped_queries:
package_generator.add_operation(query_operation)
except ParsingError as exc:
console.print(f"[red]Unable to process {gql_file.name}: {exc}")
parsing_failed = True
if parsing_failed:
continue
module_fragment = package_generator.fragments_generator.generate()
generate_result_types(directory=directory, package=package_generator, fragment=module_fragment)
for file_name in package_generator._result_types_files:
console.print(f"[green]Generated {file_name} in {directory}")