graphql-codegen 0.1.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.
- graphql_codegen/__init__.py +7 -0
- graphql_codegen/__main__.py +5 -0
- graphql_codegen/_cli/__init__.py +41 -0
- graphql_codegen/_cli/_graphql_config.py +291 -0
- graphql_codegen/_cli/_introspection.py +87 -0
- graphql_codegen/_cli/_introspection_graphql.py +53 -0
- graphql_codegen/_cli/_parsing.py +67 -0
- graphql_codegen/_cli/_schema_pointer.py +108 -0
- graphql_codegen/_cli/_source.py +68 -0
- graphql_codegen/_cli/_write.py +22 -0
- graphql_codegen/_generator/__init__.py +0 -0
- graphql_codegen/_generator/_annotation.py +149 -0
- graphql_codegen/_generator/_ast_nodes.py +235 -0
- graphql_codegen/_generator/_data_type.py +532 -0
- graphql_codegen/_generator/_document.py +108 -0
- graphql_codegen/_generator/_document_module.py +54 -0
- graphql_codegen/_generator/_imports.py +166 -0
- graphql_codegen/_generator/_injector.py +267 -0
- graphql_codegen/_generator/_merge.py +149 -0
- graphql_codegen/_generator/_naming.py +80 -0
- graphql_codegen/_generator/_operation.py +223 -0
- graphql_codegen/_generator/_scalar.py +76 -0
- graphql_codegen/_generator/_schema.py +46 -0
- graphql_codegen/_generator/_schema_type.py +334 -0
- graphql_codegen/_generator/_selection.py +246 -0
- graphql_codegen/_generator/_structs.py +87 -0
- graphql_codegen/_generator/_typed_dict.py +154 -0
- graphql_codegen/_generator/dotted_name.py +48 -0
- graphql_codegen/_generator/package.py +695 -0
- graphql_codegen/_generator/spelling.py +168 -0
- graphql_codegen/_metadata.py +11 -0
- graphql_codegen/_note.py +12 -0
- graphql_codegen/config.py +42 -0
- graphql_codegen/document_sibling_module.py +58 -0
- graphql_codegen/generate.py +19 -0
- graphql_codegen/package_location.py +41 -0
- graphql_codegen/py.typed +0 -0
- graphql_codegen/runtime/__init__.py +20 -0
- graphql_codegen/runtime/_compat.py +27 -0
- graphql_codegen/runtime/_literal.py +18 -0
- graphql_codegen/runtime/_merge.py +153 -0
- graphql_codegen/runtime/_prepare.py +250 -0
- graphql_codegen/runtime/_reflection.py +376 -0
- graphql_codegen/runtime/_sigil.py +11 -0
- graphql_codegen/runtime/_transport.py +21 -0
- graphql_codegen/runtime/client.py +384 -0
- graphql_codegen/runtime/error.py +135 -0
- graphql_codegen/runtime/injection.py +107 -0
- graphql_codegen/runtime/operation.py +135 -0
- graphql_codegen/scalar.py +59 -0
- graphql_codegen-0.1.0.dist-info/METADATA +871 -0
- graphql_codegen-0.1.0.dist-info/RECORD +55 -0
- graphql_codegen-0.1.0.dist-info/WHEEL +4 -0
- graphql_codegen-0.1.0.dist-info/entry_points.txt +3 -0
- graphql_codegen-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from graphql_codegen.config import Config as Config
|
|
2
|
+
from graphql_codegen.document_sibling_module import (
|
|
3
|
+
DocumentSiblingModule as DocumentSiblingModule,
|
|
4
|
+
)
|
|
5
|
+
from graphql_codegen.generate import generate as generate
|
|
6
|
+
from graphql_codegen.package_location import PackageLocation as PackageLocation
|
|
7
|
+
from graphql_codegen.scalar import Codec as Codec, Scalar as Scalar
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from argparse import ArgumentParser, Namespace
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import final
|
|
6
|
+
|
|
7
|
+
from graphql_codegen._cli._graphql_config import read_graphql_config
|
|
8
|
+
from graphql_codegen._cli._write import write
|
|
9
|
+
from graphql_codegen._metadata import DISTRIBUTION_NAME
|
|
10
|
+
from graphql_codegen.generate import generate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@final
|
|
14
|
+
class _Args(Namespace):
|
|
15
|
+
config_path: Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_args(argv: Sequence[str] | None, /) -> _Args:
|
|
19
|
+
parser = ArgumentParser(
|
|
20
|
+
prog=DISTRIBUTION_NAME,
|
|
21
|
+
description="Generate the package of every project of a graphql-config file.",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"config_path",
|
|
25
|
+
metavar="config",
|
|
26
|
+
type=Path,
|
|
27
|
+
help="the path to the graphql-config file, in YAML, JSON, or TOML",
|
|
28
|
+
)
|
|
29
|
+
return parser.parse_args(argv, namespace=_Args())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: Sequence[str] | None = None, /) -> None:
|
|
33
|
+
cli_args = _parse_args(argv)
|
|
34
|
+
generate_args_by_directory = read_graphql_config(cli_args.config_path)
|
|
35
|
+
packages = {
|
|
36
|
+
directory: generate(**args)
|
|
37
|
+
for directory, args in generate_args_by_directory.items()
|
|
38
|
+
}
|
|
39
|
+
for directory, files in packages.items():
|
|
40
|
+
write(directory, files)
|
|
41
|
+
print(f"Wrote {len(files)} files to `{directory}`.", file=sys.stderr)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import posixpath
|
|
4
|
+
import re
|
|
5
|
+
import tomllib
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import cache
|
|
9
|
+
from pathlib import Path, PurePosixPath
|
|
10
|
+
from typing import Final, final
|
|
11
|
+
|
|
12
|
+
from graphql import (
|
|
13
|
+
DocumentNode,
|
|
14
|
+
GraphQLError,
|
|
15
|
+
Source,
|
|
16
|
+
build_ast_schema,
|
|
17
|
+
parse,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from graphql_codegen._cli._parsing import (
|
|
21
|
+
check_all_read,
|
|
22
|
+
optional,
|
|
23
|
+
parse_at,
|
|
24
|
+
parse_dict,
|
|
25
|
+
parse_string,
|
|
26
|
+
parse_strings,
|
|
27
|
+
required,
|
|
28
|
+
)
|
|
29
|
+
from graphql_codegen._cli._schema_pointer import SchemaPointer, parse_schema_pointer
|
|
30
|
+
from graphql_codegen._cli._source import read_schema, read_sources
|
|
31
|
+
from graphql_codegen._metadata import DISTRIBUTION_NAME, METADATA
|
|
32
|
+
from graphql_codegen._note import error_note
|
|
33
|
+
from graphql_codegen.config import Config
|
|
34
|
+
from graphql_codegen.document_sibling_module import DocumentSiblingModule
|
|
35
|
+
from graphql_codegen.generate import _Params
|
|
36
|
+
from graphql_codegen.package_location import PackageLocation
|
|
37
|
+
from graphql_codegen.scalar import Codec, Scalar
|
|
38
|
+
|
|
39
|
+
_EXTENSION_NAME: Final = "pythonCodegen"
|
|
40
|
+
|
|
41
|
+
# As https://github.com/kamilkisiela/string-env-interpolation/blob/94c441088f2ba62c5f0a2a31b8b9728242184360/src/index.ts#L16, which graphql-config calls at https://github.com/graphql-hive/graphql-config/blob/3091fcc7259c2feeb9866994a6c3ca4fc6ddb7a0/src/helpers/cosmiconfig.ts#L24.
|
|
42
|
+
_ENVIRONMENT_VARIABLE: Final = re.compile(
|
|
43
|
+
r"\$\{(?P<name>[A-Z0-9_]+)(?::(?P<default>[^}]+))?\}", re.IGNORECASE
|
|
44
|
+
)
|
|
45
|
+
_QUOTED: Final = re.compile(r"\"([^\"]+)\"|'([^']+)'")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def read_graphql_config(path: Path, /) -> dict[Path, _Params]:
|
|
49
|
+
"""Return the generation arguments for each project of the graphql-config file at *path* by the directory its package goes to."""
|
|
50
|
+
load = _get_loader(path)
|
|
51
|
+
text = path.read_text(encoding="utf-8")
|
|
52
|
+
config = load(_interpolate(text, environment=os.environ))
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
directory: _read_project(project, directory=path.parent)
|
|
56
|
+
for directory, project in _parse_graphql_config(
|
|
57
|
+
config, directory=PurePosixPath(path.parent.as_posix())
|
|
58
|
+
).items()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _interpolate(text: str, /, *, environment: Mapping[str, str]) -> str:
|
|
63
|
+
def replacement(match: re.Match[str], /) -> str:
|
|
64
|
+
name = match["name"]
|
|
65
|
+
|
|
66
|
+
# An empty value falls back to the default too.
|
|
67
|
+
if value := environment.get(name):
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
if isinstance(default := match["default"], str):
|
|
71
|
+
stripped = default.strip()
|
|
72
|
+
quoted = _QUOTED.fullmatch(stripped)
|
|
73
|
+
# One holding a colon may be quoted, in single or double quotes.
|
|
74
|
+
return (
|
|
75
|
+
stripped
|
|
76
|
+
if quoted is None
|
|
77
|
+
else next(group for group in quoted.groups() if isinstance(group, str))
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Expected the environment variable `{name}` to have a value, since `{match[0]}` has no default."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return _ENVIRONMENT_VARIABLE.sub(replacement, text)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_loader(config_path: Path, /) -> Callable[[str], object]:
|
|
88
|
+
match config_path.suffix:
|
|
89
|
+
case ".json":
|
|
90
|
+
return json.loads
|
|
91
|
+
case ".toml":
|
|
92
|
+
return tomllib.loads
|
|
93
|
+
case ".yaml" | ".yml" | "":
|
|
94
|
+
# `.graphqlrc` may be YAML or JSON, which YAML reads too.
|
|
95
|
+
try:
|
|
96
|
+
import yaml # noqa: PLC0415
|
|
97
|
+
except ImportError as error:
|
|
98
|
+
raise ImportError(
|
|
99
|
+
f"`{config_path}` is YAML: install `{DISTRIBUTION_NAME}[{_yaml_extra_name()}]` to read it."
|
|
100
|
+
) from error
|
|
101
|
+
|
|
102
|
+
return yaml.safe_load
|
|
103
|
+
case ".js" | ".cjs" | ".mjs" | ".ts" | ".cts" | ".mts":
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"`{config_path}` is JavaScript or TypeScript, which only JavaScript can evaluate: write it in YAML, JSON, or TOML."
|
|
106
|
+
)
|
|
107
|
+
case suffix:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Cannot tell the format of `{config_path}` from `{suffix}`: expected YAML, JSON, or TOML."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@cache
|
|
114
|
+
def _yaml_extra_name() -> str:
|
|
115
|
+
name = "yaml"
|
|
116
|
+
assert name in METADATA.get_all("Provides-Extra", ()), (
|
|
117
|
+
f"Expected `pyproject.toml` to declare the `{name}` extra."
|
|
118
|
+
)
|
|
119
|
+
return name
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@final
|
|
123
|
+
@dataclass(frozen=True, kw_only=True)
|
|
124
|
+
class _Project:
|
|
125
|
+
name: str
|
|
126
|
+
schema: SchemaPointer
|
|
127
|
+
documents: Sequence[str]
|
|
128
|
+
package_location: PackageLocation
|
|
129
|
+
config: Config
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _read_project(project: _Project, /, *, directory: Path) -> _Params:
|
|
133
|
+
with error_note(f"In project `{project.name}`."):
|
|
134
|
+
return _parse_project(
|
|
135
|
+
project,
|
|
136
|
+
schema=read_schema(project.schema, directory=directory),
|
|
137
|
+
documents=read_sources(project.documents, directory=directory),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _parse_project(
|
|
142
|
+
project: _Project,
|
|
143
|
+
/,
|
|
144
|
+
*,
|
|
145
|
+
schema: Sequence[Source],
|
|
146
|
+
documents: Sequence[Source],
|
|
147
|
+
) -> _Params:
|
|
148
|
+
try:
|
|
149
|
+
built_schema = build_ast_schema(_parse_sources(schema))
|
|
150
|
+
except (GraphQLError, TypeError) as error:
|
|
151
|
+
# `build_ast_schema()` raises a `TypeError` for an invalid schema.
|
|
152
|
+
raise ValueError(f"Cannot build the schema: {error}") from error
|
|
153
|
+
|
|
154
|
+
return _Params(
|
|
155
|
+
document=_parse_sources(documents),
|
|
156
|
+
schema=built_schema,
|
|
157
|
+
config=project.config,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _parse_sources(sources: Sequence[Source], /) -> DocumentNode:
|
|
162
|
+
"""Parse *sources* into one document, since a fragment defined in one file may be spread in another, and a schema's types spread over several files."""
|
|
163
|
+
return DocumentNode(
|
|
164
|
+
definitions=tuple(
|
|
165
|
+
definition for source in sources for definition in parse(source).definitions
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_graphql_config(
|
|
171
|
+
config: object, /, *, directory: PurePosixPath
|
|
172
|
+
) -> dict[Path, _Project]:
|
|
173
|
+
"""The projects of *config*, the graphql-config file in *directory*, by the directory each package goes to."""
|
|
174
|
+
root = parse_dict(config)
|
|
175
|
+
entries = (
|
|
176
|
+
required(root, "projects", parse_dict)
|
|
177
|
+
if "projects" in root
|
|
178
|
+
else {"default": root}
|
|
179
|
+
)
|
|
180
|
+
projects: dict[Path, _Project] = {}
|
|
181
|
+
|
|
182
|
+
for name, entry in entries.items():
|
|
183
|
+
with error_note(f"In project `{name}`."):
|
|
184
|
+
project = _project(entry, name=name, directory=directory)
|
|
185
|
+
|
|
186
|
+
if project is None:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Derived from both, which needs no IO, unlike finding where packages start from a directory.
|
|
190
|
+
|
|
191
|
+
package_directory = Path(project.package_location._package_directory)
|
|
192
|
+
|
|
193
|
+
if (other := projects.get(package_directory)) is not None:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"Expected `{other.name}` and `{name}` to generate into directories of their own, but both generate into `{package_directory}`."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
projects[package_directory] = project
|
|
199
|
+
|
|
200
|
+
if not projects:
|
|
201
|
+
raise ValueError(f"Expected a project with a `{_EXTENSION_NAME}` extension.")
|
|
202
|
+
|
|
203
|
+
return projects
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _project(
|
|
207
|
+
value: object, /, *, name: str, directory: PurePosixPath
|
|
208
|
+
) -> _Project | None:
|
|
209
|
+
entry = parse_dict(value)
|
|
210
|
+
extensions = optional(entry, "extensions", parse_dict, default={})
|
|
211
|
+
|
|
212
|
+
if (extension := extensions.get(_EXTENSION_NAME)) is None:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
schema = required(entry, "schema", parse_schema_pointer)
|
|
216
|
+
documents = required(entry, "documents", parse_strings)
|
|
217
|
+
package_location, config = parse_at(
|
|
218
|
+
extension,
|
|
219
|
+
key=f"extensions.{_EXTENSION_NAME}",
|
|
220
|
+
parse=lambda value: _extension(value, directory=directory),
|
|
221
|
+
)
|
|
222
|
+
return _Project(
|
|
223
|
+
name=name,
|
|
224
|
+
schema=schema,
|
|
225
|
+
documents=documents,
|
|
226
|
+
package_location=package_location,
|
|
227
|
+
config=config,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _extension(
|
|
232
|
+
value: object, /, *, directory: PurePosixPath
|
|
233
|
+
) -> tuple[PackageLocation, Config]:
|
|
234
|
+
unread = parse_dict(value)
|
|
235
|
+
|
|
236
|
+
module_root = required(unread, "moduleRoot", parse_string)
|
|
237
|
+
package_location = PackageLocation(
|
|
238
|
+
module_root=PurePosixPath(posixpath.normpath(directory / module_root)),
|
|
239
|
+
package=required(unread, "package", parse_string),
|
|
240
|
+
)
|
|
241
|
+
document_sibling_module = optional(
|
|
242
|
+
unread,
|
|
243
|
+
"documentSiblingModule",
|
|
244
|
+
lambda value: DocumentSiblingModule(
|
|
245
|
+
name=parse_string(value), package_location=package_location
|
|
246
|
+
),
|
|
247
|
+
default=None,
|
|
248
|
+
)
|
|
249
|
+
injector_names = optional(
|
|
250
|
+
unread, "injectorNames", parse_strings, default=frozenset()
|
|
251
|
+
)
|
|
252
|
+
non_null_directive_name = optional(
|
|
253
|
+
unread, "nonNullDirectiveName", parse_string, default=None
|
|
254
|
+
)
|
|
255
|
+
scalars = optional(unread, "scalars", _scalars, default={})
|
|
256
|
+
struct_interface_name = optional(
|
|
257
|
+
unread, "structInterfaceName", parse_string, default=None
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
check_all_read(unread)
|
|
261
|
+
return package_location, Config(
|
|
262
|
+
document_sibling_module=document_sibling_module,
|
|
263
|
+
injector_names=injector_names,
|
|
264
|
+
non_null_directive_name=non_null_directive_name,
|
|
265
|
+
scalars=scalars,
|
|
266
|
+
struct_interface_name=struct_interface_name,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _scalars(value: object, /) -> dict[str, Scalar]:
|
|
271
|
+
scalars = parse_dict(value)
|
|
272
|
+
return {
|
|
273
|
+
scalar_name: required(scalars, scalar_name, _scalar)
|
|
274
|
+
for scalar_name in list(scalars)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _scalar(value: object, /) -> Scalar:
|
|
279
|
+
unread = parse_dict(value)
|
|
280
|
+
type_ = required(unread, "type", parse_string)
|
|
281
|
+
codec = optional(unread, "codec", _codec, default=None)
|
|
282
|
+
check_all_read(unread)
|
|
283
|
+
return Scalar(type=type_, codec=codec)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _codec(value: object, /) -> Codec:
|
|
287
|
+
unread = parse_dict(value)
|
|
288
|
+
decode = required(unread, "decode", parse_string)
|
|
289
|
+
encode = required(unread, "encode", parse_string)
|
|
290
|
+
check_all_read(unread)
|
|
291
|
+
return Codec(decode=decode, encode=encode)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from collections.abc import Callable, Mapping
|
|
2
|
+
from http.client import HTTPResponse
|
|
3
|
+
from mimetypes import types_map
|
|
4
|
+
from typing import Final, Literal, TypeAlias
|
|
5
|
+
from urllib.request import Request, urlopen
|
|
6
|
+
|
|
7
|
+
from graphql import (
|
|
8
|
+
IntrospectionQuery,
|
|
9
|
+
build_client_schema,
|
|
10
|
+
get_introspection_query,
|
|
11
|
+
print_schema,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from graphql_codegen._cli._introspection_graphql import (
|
|
15
|
+
FieldNames,
|
|
16
|
+
IntrospectionProbe,
|
|
17
|
+
IntrospectionProbeData,
|
|
18
|
+
)
|
|
19
|
+
from graphql_codegen._metadata import DISTRIBUTION_NAME, METADATA
|
|
20
|
+
from graphql_codegen.runtime import Client, Operation
|
|
21
|
+
|
|
22
|
+
_JSON: Final = types_map[".json"]
|
|
23
|
+
_GRAPHQL_RESPONSE: Final = "application/graphql-response+json"
|
|
24
|
+
|
|
25
|
+
_TIMEOUT: Final = 30.0 # graphql-config has no option for it.
|
|
26
|
+
"""Seconds to wait for each response."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _field_names(type_: FieldNames, /) -> frozenset[str]:
|
|
30
|
+
return frozenset(field["name"] for field in type_["fields"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Not a `type` statement, which could not be called to build the operation.
|
|
34
|
+
_IntrospectionOperation: TypeAlias = Operation[ # noqa: UP040
|
|
35
|
+
Literal["query"], dict[str, object], IntrospectionQuery
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _introspection_operation(
|
|
40
|
+
probe: IntrospectionProbeData, /
|
|
41
|
+
) -> _IntrospectionOperation:
|
|
42
|
+
type_fields = _field_names(probe["type"])
|
|
43
|
+
return _IntrospectionOperation(
|
|
44
|
+
operation_type="query",
|
|
45
|
+
name="IntrospectionQuery",
|
|
46
|
+
document=get_introspection_query(
|
|
47
|
+
specified_by_url="specifiedByURL" in type_fields,
|
|
48
|
+
directive_is_repeatable="isRepeatable" in _field_names(probe["directive"]),
|
|
49
|
+
schema_description="description" in _field_names(probe["schema"]),
|
|
50
|
+
input_value_deprecation="isDeprecated" in _field_names(probe["inputValue"]),
|
|
51
|
+
one_of="isOneOf" in type_fields,
|
|
52
|
+
),
|
|
53
|
+
variables_type=dict[str, object],
|
|
54
|
+
# graphql-core's `IntrospectionQuery` uses `DirectiveLocation`, which it imports only `if TYPE_CHECKING:`.
|
|
55
|
+
data_type=dict[str, object], # ty: ignore[invalid-argument-type]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fetch_sdl(url: str, /, *, headers: Mapping[str, str]) -> str:
|
|
60
|
+
client = Client(_transport(url, headers=headers))
|
|
61
|
+
probe = client(IntrospectionProbe({}))
|
|
62
|
+
introspection_operation = _introspection_operation(probe)
|
|
63
|
+
introspection_data = client(introspection_operation({}))
|
|
64
|
+
return print_schema(build_client_schema(introspection=introspection_data))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _transport(url: str, /, *, headers: Mapping[str, str]) -> Callable[[bytes], bytes]:
|
|
68
|
+
def transport(body: bytes, /) -> bytes:
|
|
69
|
+
request = Request( # noqa: S310
|
|
70
|
+
url,
|
|
71
|
+
data=body,
|
|
72
|
+
headers={
|
|
73
|
+
"Accept": f"{_GRAPHQL_RESPONSE}, {_JSON}",
|
|
74
|
+
"Content-Type": _JSON,
|
|
75
|
+
"User-Agent": f"{DISTRIBUTION_NAME}/{METADATA['Version']}",
|
|
76
|
+
**headers,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
response = urlopen(request, timeout=_TIMEOUT) # noqa: S310
|
|
81
|
+
# The only response an `http:` or `https:` URL gives.
|
|
82
|
+
assert isinstance(response, HTTPResponse)
|
|
83
|
+
|
|
84
|
+
with response:
|
|
85
|
+
return response.read()
|
|
86
|
+
|
|
87
|
+
return transport
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Code generated by graphql-codegen from `_introspection.graphql`. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
import builtins as _builtins
|
|
4
|
+
import typing as _typing
|
|
5
|
+
from graphql_codegen.runtime import _compat
|
|
6
|
+
from graphql_codegen.runtime import _reflection
|
|
7
|
+
from graphql_codegen import runtime as _runtime
|
|
8
|
+
|
|
9
|
+
class _FieldNames_fields(_compat.TypedDict, closed=True):
|
|
10
|
+
"""Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type."""
|
|
11
|
+
name: _builtins.str
|
|
12
|
+
|
|
13
|
+
class FieldNames(_compat.TypedDict):
|
|
14
|
+
"""The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.
|
|
15
|
+
|
|
16
|
+
Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types."""
|
|
17
|
+
fields: _typing.Annotated[_builtins.list[_FieldNames_fields], _reflection.NON_NULL]
|
|
18
|
+
|
|
19
|
+
class IntrospectionProbeVariables(_compat.TypedDict, closed=True):
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
class IntrospectionProbeData(_compat.TypedDict, closed=True):
|
|
23
|
+
schema: _typing.Annotated[FieldNames, _reflection.NON_NULL]
|
|
24
|
+
"""Request the type information of a single type."""
|
|
25
|
+
type: _typing.Annotated[FieldNames, _reflection.NON_NULL]
|
|
26
|
+
"""Request the type information of a single type."""
|
|
27
|
+
inputValue: _typing.Annotated[FieldNames, _reflection.NON_NULL]
|
|
28
|
+
"""Request the type information of a single type."""
|
|
29
|
+
directive: _typing.Annotated[FieldNames, _reflection.NON_NULL]
|
|
30
|
+
"""Request the type information of a single type."""
|
|
31
|
+
|
|
32
|
+
IntrospectionProbe: _runtime.Operation[_typing.Literal['query'], IntrospectionProbeVariables, IntrospectionProbeData] = _runtime.Operation(operation_type='query', name='IntrospectionProbe', document="""query IntrospectionProbe {
|
|
33
|
+
schema§: __type(name: "__Schema") {
|
|
34
|
+
...FieldNames
|
|
35
|
+
}
|
|
36
|
+
type§: __type(name: "__Type") {
|
|
37
|
+
...FieldNames
|
|
38
|
+
}
|
|
39
|
+
inputValue§: __type(name: "__InputValue") {
|
|
40
|
+
...FieldNames
|
|
41
|
+
}
|
|
42
|
+
directive§: __type(name: "__Directive") {
|
|
43
|
+
...FieldNames
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fragment FieldNames on __Type {
|
|
48
|
+
fields {
|
|
49
|
+
name
|
|
50
|
+
}
|
|
51
|
+
}""", variables_type=IntrospectionProbeVariables, data_type=IntrospectionProbeData)
|
|
52
|
+
"""Which fields the server's introspection types have.
|
|
53
|
+
The full introspection query then asks only for those, since an older server rejects a field it does not know."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Parse the values of a config file, noting where each error is."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
from graphql_codegen._note import error_note
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_all_read(unread: dict[str, object], /) -> None:
|
|
10
|
+
if unread:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
f"Expected no other key, but got {', '.join(f'`{key}`' for key in sorted(unread))}."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_dict(value: object, /) -> dict[str, object]:
|
|
17
|
+
"""Return a shallow-copy of *value*."""
|
|
18
|
+
if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value):
|
|
19
|
+
raise TypeError(f"Expected a mapping with string keys, but got `{value!r}`.")
|
|
20
|
+
|
|
21
|
+
return dict(cast(Mapping[str, object], value))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def required[T](
|
|
25
|
+
unread: dict[str, object], key: str, parse: Callable[[object], T], /
|
|
26
|
+
) -> T:
|
|
27
|
+
value = optional(unread, key, parse, default=None)
|
|
28
|
+
|
|
29
|
+
if value is None:
|
|
30
|
+
raise ValueError(f"Expected `{key}` to be set.")
|
|
31
|
+
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def optional[T](
|
|
36
|
+
unread: dict[str, object], key: str, parse: Callable[[object], T], /, *, default: T
|
|
37
|
+
) -> T:
|
|
38
|
+
value = unread.pop(key, None)
|
|
39
|
+
return default if value is None else parse_at(value, key=key, parse=parse)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_at[T](value: object, /, *, key: str, parse: Callable[[object], T]) -> T:
|
|
43
|
+
with error_note(f"In `{key}`."):
|
|
44
|
+
return parse(value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_strings(value: object, /) -> list[str]:
|
|
48
|
+
if isinstance(value, str):
|
|
49
|
+
return [value]
|
|
50
|
+
|
|
51
|
+
if not isinstance(value, list):
|
|
52
|
+
raise TypeError(f"Expected a string or a list of strings, but got `{value!r}`.")
|
|
53
|
+
|
|
54
|
+
strings: list[str] = []
|
|
55
|
+
|
|
56
|
+
for index, item in enumerate(value):
|
|
57
|
+
with error_note(f"At index {index}."):
|
|
58
|
+
strings.append(parse_string(item))
|
|
59
|
+
|
|
60
|
+
return strings
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_string(value: object, /) -> str:
|
|
64
|
+
if not isinstance(value, str):
|
|
65
|
+
raise TypeError(f"Expected a string, but got `{value!r}`.")
|
|
66
|
+
|
|
67
|
+
return value
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
from typing import cast, final
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
from graphql_codegen._cli._parsing import (
|
|
8
|
+
check_all_read,
|
|
9
|
+
optional,
|
|
10
|
+
parse_at,
|
|
11
|
+
parse_dict,
|
|
12
|
+
parse_string,
|
|
13
|
+
required,
|
|
14
|
+
)
|
|
15
|
+
from graphql_codegen._note import error_note
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
@dataclass(frozen=True, kw_only=True)
|
|
20
|
+
class Globs:
|
|
21
|
+
patterns: tuple[str, ...]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@final
|
|
25
|
+
@dataclass(frozen=True, kw_only=True)
|
|
26
|
+
class IntrospectionResult:
|
|
27
|
+
path: PurePosixPath
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@final
|
|
31
|
+
@dataclass(frozen=True, kw_only=True)
|
|
32
|
+
class Endpoint:
|
|
33
|
+
url: str
|
|
34
|
+
headers: Mapping[str, str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
type SchemaPointer = Globs | IntrospectionResult | Endpoint
|
|
38
|
+
"""Where a project's schema comes from, in one of the forms https://the-guild.dev/graphql/config/docs/user/schema lists."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_schema_pointer(value: object, /) -> SchemaPointer:
|
|
42
|
+
match value:
|
|
43
|
+
case [item]:
|
|
44
|
+
with error_note("At index 0."):
|
|
45
|
+
return _parse_single_pointer(item)
|
|
46
|
+
case list():
|
|
47
|
+
patterns: list[str] = []
|
|
48
|
+
|
|
49
|
+
for index, item in enumerate(value):
|
|
50
|
+
with error_note(f"At index {index}."):
|
|
51
|
+
pointer = _parse_single_pointer(item)
|
|
52
|
+
|
|
53
|
+
if not isinstance(pointer, Globs):
|
|
54
|
+
raise TypeError(
|
|
55
|
+
f"Expected a glob of SDL files, since the list holds more than one item, but got `{item}`."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# A single item is one glob.
|
|
59
|
+
(pattern,) = pointer.patterns
|
|
60
|
+
patterns.append(pattern)
|
|
61
|
+
|
|
62
|
+
return Globs(patterns=tuple(patterns))
|
|
63
|
+
case _:
|
|
64
|
+
return _parse_single_pointer(value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_single_pointer(value: object, /) -> SchemaPointer:
|
|
68
|
+
if isinstance(value, str):
|
|
69
|
+
if (url := _parse_url(value)) is not None:
|
|
70
|
+
return Endpoint(url=url, headers={})
|
|
71
|
+
|
|
72
|
+
if (path := PurePosixPath(value)).suffix == ".json":
|
|
73
|
+
return IntrospectionResult(path=path)
|
|
74
|
+
|
|
75
|
+
return Globs(patterns=(value,))
|
|
76
|
+
|
|
77
|
+
if isinstance(value, Mapping) and len(value) == 1:
|
|
78
|
+
((key, options),) = cast(Mapping[object, object], value).items()
|
|
79
|
+
|
|
80
|
+
if isinstance(key, str) and (url := _parse_url(key)) is not None:
|
|
81
|
+
return Endpoint(
|
|
82
|
+
url=url,
|
|
83
|
+
headers={}
|
|
84
|
+
if options is None
|
|
85
|
+
else parse_at(options, key=key, parse=_parse_endpoint_headers),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
raise TypeError(
|
|
89
|
+
f"Expected a glob, a `.json` path, or a URL (possibly mapped to its options), but got `{value!r}`."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_url(value: str, /) -> str | None:
|
|
94
|
+
split_result = urlsplit(value)
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
split_result.geturl()
|
|
98
|
+
# `urlsplit()` lowercases the scheme, which `geturl()` keeps.
|
|
99
|
+
if split_result.scheme in {"http", "https"}
|
|
100
|
+
else None
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse_endpoint_headers(value: object, /) -> dict[str, str]:
|
|
105
|
+
unread = parse_dict(value)
|
|
106
|
+
headers = optional(unread, "headers", parse_dict, default={})
|
|
107
|
+
check_all_read(unread)
|
|
108
|
+
return {name: required(headers, name, parse_string) for name in list(headers)}
|