gql 3.6.0b3__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.
gql/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """The primary :mod:`gql` package includes everything you need to
2
+ execute GraphQL requests, with the exception of the transports
3
+ which are optional:
4
+
5
+ - the :func:`gql <gql.gql>` method to parse a GraphQL query
6
+ - the :class:`Client <gql.Client>` class as the entrypoint to execute requests
7
+ and create sessions
8
+ """
9
+
10
+ from .__version__ import __version__
11
+ from .client import Client
12
+ from .gql import gql
13
+ from .graphql_request import GraphQLRequest
14
+
15
+ __all__ = [
16
+ "__version__",
17
+ "gql",
18
+ "Client",
19
+ "GraphQLRequest",
20
+ ]
gql/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "3.6.0b3"
gql/cli.py ADDED
@@ -0,0 +1,558 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import signal as signal_module
5
+ import sys
6
+ import textwrap
7
+ from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
8
+ from typing import Any, Dict, Optional
9
+
10
+ from graphql import GraphQLError, print_schema
11
+ from yarl import URL
12
+
13
+ from gql import Client, __version__, gql
14
+ from gql.transport import AsyncTransport
15
+ from gql.transport.exceptions import TransportQueryError
16
+
17
+ description = """
18
+ Send GraphQL queries from the command line using http(s) or websockets.
19
+ If used interactively, write your query, then use Ctrl-D (EOF) to execute it.
20
+ """
21
+
22
+ examples = """
23
+ EXAMPLES
24
+ ========
25
+
26
+ # Simple query using https
27
+ echo 'query { continent(code:"AF") { name } }' | \
28
+ gql-cli https://countries.trevorblades.com
29
+
30
+ # Simple query using websockets
31
+ echo 'query { continent(code:"AF") { name } }' | \
32
+ gql-cli wss://countries.trevorblades.com/graphql
33
+
34
+ # Query with variable
35
+ echo 'query getContinent($code:ID!) { continent(code:$code) { name } }' | \
36
+ gql-cli https://countries.trevorblades.com --variables code:AF
37
+
38
+ # Interactive usage (insert your query in the terminal, then press Ctrl-D to execute it)
39
+ gql-cli wss://countries.trevorblades.com/graphql --variables code:AF
40
+
41
+ # Execute query saved in a file
42
+ cat query.gql | gql-cli wss://countries.trevorblades.com/graphql
43
+
44
+ # Print the schema of the backend
45
+ gql-cli https://countries.trevorblades.com/graphql --print-schema
46
+
47
+ """
48
+
49
+
50
+ def positive_int_or_none(value_str: str) -> Optional[int]:
51
+ """Convert a string argument value into either an int or None.
52
+
53
+ Raise a ValueError if the argument is negative or a string which is not "none"
54
+ """
55
+ try:
56
+ value_int = int(value_str)
57
+ except ValueError:
58
+ if value_str.lower() == "none":
59
+ return None
60
+ else:
61
+ raise
62
+
63
+ if value_int < 0:
64
+ raise ValueError
65
+
66
+ return value_int
67
+
68
+
69
+ def get_parser(with_examples: bool = False) -> ArgumentParser:
70
+ """Provides an ArgumentParser for the gql-cli script.
71
+
72
+ This function is also used by sphinx to generate the script documentation.
73
+
74
+ :param with_examples: set to False by default so that the examples are not
75
+ present in the sphinx docs (they are put there with
76
+ a different layout)
77
+ """
78
+
79
+ parser = ArgumentParser(
80
+ description=description,
81
+ epilog=examples if with_examples else None,
82
+ formatter_class=RawTextHelpFormatter,
83
+ )
84
+ parser.add_argument(
85
+ "server", help="the server url starting with http://, https://, ws:// or wss://"
86
+ )
87
+ parser.add_argument(
88
+ "-V",
89
+ "--variables",
90
+ nargs="*",
91
+ help="query variables in the form key:json_value",
92
+ )
93
+ parser.add_argument(
94
+ "-H", "--headers", nargs="*", help="http headers in the form key:value"
95
+ )
96
+ parser.add_argument("--version", action="version", version=f"v{__version__}")
97
+ group = parser.add_mutually_exclusive_group()
98
+ group.add_argument(
99
+ "-d",
100
+ "--debug",
101
+ help="print lots of debugging statements (loglevel==DEBUG)",
102
+ action="store_const",
103
+ dest="loglevel",
104
+ const=logging.DEBUG,
105
+ )
106
+ group.add_argument(
107
+ "-v",
108
+ "--verbose",
109
+ help="show low level messages (loglevel==INFO)",
110
+ action="store_const",
111
+ dest="loglevel",
112
+ const=logging.INFO,
113
+ )
114
+ parser.add_argument(
115
+ "-o",
116
+ "--operation-name",
117
+ help="set the operation_name value",
118
+ dest="operation_name",
119
+ )
120
+ parser.add_argument(
121
+ "--print-schema",
122
+ help="get the schema from instrospection and print it",
123
+ action="store_true",
124
+ dest="print_schema",
125
+ )
126
+ parser.add_argument(
127
+ "--schema-download",
128
+ nargs="*",
129
+ help=textwrap.dedent(
130
+ """select the introspection query arguments to download the schema.
131
+ Only useful if --print-schema is used.
132
+ By default, it will:
133
+
134
+ - request field descriptions
135
+ - not request deprecated input fields
136
+
137
+ Possible options:
138
+
139
+ - descriptions:false for a compact schema without comments
140
+ - input_value_deprecation:true to download deprecated input fields
141
+ - specified_by_url:true
142
+ - schema_description:true
143
+ - directive_is_repeatable:true"""
144
+ ),
145
+ dest="schema_download",
146
+ )
147
+ parser.add_argument(
148
+ "--execute-timeout",
149
+ help="set the execute_timeout argument of the Client (default: 10)",
150
+ type=positive_int_or_none,
151
+ default=10,
152
+ dest="execute_timeout",
153
+ )
154
+ parser.add_argument(
155
+ "--transport",
156
+ default="auto",
157
+ choices=[
158
+ "auto",
159
+ "aiohttp",
160
+ "httpx",
161
+ "phoenix",
162
+ "websockets",
163
+ "aiohttp_websockets",
164
+ "appsync_http",
165
+ "appsync_websockets",
166
+ ],
167
+ help=(
168
+ "select the transport. 'auto' by default: "
169
+ "aiohttp or websockets depending on url scheme"
170
+ ),
171
+ dest="transport",
172
+ )
173
+
174
+ appsync_description = """
175
+ By default, for an AppSync backend, the IAM authentication is chosen.
176
+
177
+ If you want API key or JWT authentication, you can provide one of the
178
+ following arguments:"""
179
+
180
+ appsync_group = parser.add_argument_group(
181
+ "AWS AppSync options", description=appsync_description
182
+ )
183
+
184
+ appsync_auth_group = appsync_group.add_mutually_exclusive_group()
185
+
186
+ appsync_auth_group.add_argument(
187
+ "--api-key",
188
+ help="Provide an API key for authentication",
189
+ dest="api_key",
190
+ )
191
+
192
+ appsync_auth_group.add_argument(
193
+ "--jwt",
194
+ help="Provide an JSON Web token for authentication",
195
+ dest="jwt",
196
+ )
197
+
198
+ return parser
199
+
200
+
201
+ def get_transport_args(args: Namespace) -> Dict[str, Any]:
202
+ """Extract extra arguments necessary for the transport
203
+ from the parsed command line args
204
+
205
+ Will create a headers dict by splitting the colon
206
+ in the --headers arguments
207
+
208
+ :param args: parsed command line arguments
209
+ """
210
+
211
+ transport_args: Dict[str, Any] = {}
212
+
213
+ # Parse the headers argument
214
+ headers = {}
215
+ if args.headers is not None:
216
+ for header in args.headers:
217
+
218
+ try:
219
+ # Split only the first colon (throw a ValueError if no colon is present)
220
+ header_key, header_value = header.split(":", 1)
221
+
222
+ headers[header_key] = header_value
223
+
224
+ except ValueError:
225
+ raise ValueError(f"Invalid header: {header}")
226
+
227
+ if args.headers is not None:
228
+ transport_args["headers"] = headers
229
+
230
+ return transport_args
231
+
232
+
233
+ def get_execute_args(args: Namespace) -> Dict[str, Any]:
234
+ """Extract extra arguments necessary for the execute or subscribe
235
+ methods from the parsed command line args
236
+
237
+ Extract the operation_name
238
+
239
+ Extract the variable_values from the --variables argument
240
+ by splitting the first colon, then loads the json value,
241
+ We try to add double quotes around the value if it does not work first
242
+ in order to simplify the passing of simple string values
243
+ (we allow --variables KEY:VALUE instead of KEY:\"VALUE\")
244
+
245
+ :param args: parsed command line arguments
246
+ """
247
+
248
+ execute_args: Dict[str, Any] = {}
249
+
250
+ # Parse the operation_name argument
251
+ if args.operation_name is not None:
252
+ execute_args["operation_name"] = args.operation_name
253
+
254
+ # Parse the variables argument
255
+ if args.variables is not None:
256
+
257
+ variables = {}
258
+
259
+ for var in args.variables:
260
+
261
+ try:
262
+ # Split only the first colon
263
+ # (throw a ValueError if no colon is present)
264
+ variable_key, variable_json_value = var.split(":", 1)
265
+
266
+ # Extract the json value,
267
+ # trying with double quotes if it does not work
268
+ try:
269
+ variable_value = json.loads(variable_json_value)
270
+ except json.JSONDecodeError:
271
+ try:
272
+ variable_value = json.loads(f'"{variable_json_value}"')
273
+ except json.JSONDecodeError:
274
+ raise ValueError
275
+
276
+ # Save the value in the variables dict
277
+ variables[variable_key] = variable_value
278
+
279
+ except ValueError:
280
+ raise ValueError(f"Invalid variable: {var}")
281
+
282
+ execute_args["variable_values"] = variables
283
+
284
+ return execute_args
285
+
286
+
287
+ def autodetect_transport(url: URL) -> str:
288
+ """Detects which transport should be used depending on url."""
289
+
290
+ if url.scheme in ["ws", "wss"]:
291
+ try:
292
+ import websockets # noqa: F401
293
+
294
+ transport_name = "websockets"
295
+ except ImportError: # pragma: no cover
296
+ transport_name = "aiohttp_websockets"
297
+
298
+ else:
299
+ assert url.scheme in ["http", "https"]
300
+ transport_name = "aiohttp"
301
+
302
+ return transport_name
303
+
304
+
305
+ def get_transport(args: Namespace) -> Optional[AsyncTransport]:
306
+ """Instantiate a transport from the parsed command line arguments
307
+
308
+ :param args: parsed command line arguments
309
+ """
310
+
311
+ # Get the url scheme from server parameter
312
+ url = URL(args.server)
313
+
314
+ # Validate scheme
315
+ if url.scheme not in ["http", "https", "ws", "wss"]:
316
+ raise ValueError("URL protocol should be one of: http, https, ws, wss")
317
+
318
+ # Get extra transport parameters from command line arguments
319
+ # (headers)
320
+ transport_args = get_transport_args(args)
321
+
322
+ # Either use the requested transport or autodetect it
323
+ if args.transport == "auto":
324
+ transport_name = autodetect_transport(url)
325
+ else:
326
+ transport_name = args.transport
327
+
328
+ # Import the correct transport class depending on the transport name
329
+ if transport_name == "aiohttp":
330
+ from gql.transport.aiohttp import AIOHTTPTransport
331
+
332
+ return AIOHTTPTransport(url=args.server, **transport_args)
333
+
334
+ elif transport_name == "httpx":
335
+ from gql.transport.httpx import HTTPXAsyncTransport
336
+
337
+ return HTTPXAsyncTransport(url=args.server, **transport_args)
338
+
339
+ elif transport_name == "phoenix":
340
+ from gql.transport.phoenix_channel_websockets import (
341
+ PhoenixChannelWebsocketsTransport,
342
+ )
343
+
344
+ return PhoenixChannelWebsocketsTransport(url=args.server, **transport_args)
345
+
346
+ elif transport_name == "websockets":
347
+ from gql.transport.websockets import WebsocketsTransport
348
+
349
+ transport_args["ssl"] = url.scheme == "wss"
350
+
351
+ return WebsocketsTransport(url=args.server, **transport_args)
352
+
353
+ elif transport_name == "aiohttp_websockets":
354
+ from gql.transport.aiohttp_websockets import AIOHTTPWebsocketsTransport
355
+
356
+ return AIOHTTPWebsocketsTransport(url=args.server, **transport_args)
357
+
358
+ else:
359
+
360
+ from gql.transport.appsync_auth import AppSyncAuthentication
361
+
362
+ assert transport_name in ["appsync_http", "appsync_websockets"]
363
+ assert url.host is not None
364
+
365
+ auth: AppSyncAuthentication
366
+
367
+ if args.api_key:
368
+ from gql.transport.appsync_auth import AppSyncApiKeyAuthentication
369
+
370
+ auth = AppSyncApiKeyAuthentication(host=url.host, api_key=args.api_key)
371
+
372
+ elif args.jwt:
373
+ from gql.transport.appsync_auth import AppSyncJWTAuthentication
374
+
375
+ auth = AppSyncJWTAuthentication(host=url.host, jwt=args.jwt)
376
+
377
+ else:
378
+ from gql.transport.appsync_auth import AppSyncIAMAuthentication
379
+ from botocore.exceptions import NoRegionError
380
+
381
+ try:
382
+ auth = AppSyncIAMAuthentication(host=url.host)
383
+ except NoRegionError:
384
+ # A warning message has been printed in the console
385
+ return None
386
+
387
+ transport_args["auth"] = auth
388
+
389
+ if transport_name == "appsync_http":
390
+ from gql.transport.aiohttp import AIOHTTPTransport
391
+
392
+ return AIOHTTPTransport(url=args.server, **transport_args)
393
+
394
+ else:
395
+ from gql.transport.appsync_websockets import AppSyncWebsocketsTransport
396
+
397
+ try:
398
+ return AppSyncWebsocketsTransport(url=args.server, **transport_args)
399
+ except Exception:
400
+ # This is for the NoCredentialsError but we cannot import it here
401
+ return None
402
+
403
+
404
+ def get_introspection_args(args: Namespace) -> Dict:
405
+ """Get the introspection args depending on the schema_download argument"""
406
+
407
+ # Parse the headers argument
408
+ introspection_args = {}
409
+
410
+ possible_args = [
411
+ "descriptions",
412
+ "specified_by_url",
413
+ "directive_is_repeatable",
414
+ "schema_description",
415
+ "input_value_deprecation",
416
+ ]
417
+
418
+ if args.schema_download is not None:
419
+ for arg in args.schema_download:
420
+
421
+ try:
422
+ # Split only the first colon (throw a ValueError if no colon is present)
423
+ arg_key, arg_value = arg.split(":", 1)
424
+
425
+ if arg_key not in possible_args:
426
+ raise ValueError(f"Invalid schema_download: {args.schema_download}")
427
+
428
+ arg_value = arg_value.lower()
429
+ if arg_value not in ["true", "false"]:
430
+ raise ValueError(f"Invalid schema_download: {args.schema_download}")
431
+
432
+ introspection_args[arg_key] = arg_value == "true"
433
+
434
+ except ValueError:
435
+ raise ValueError(f"Invalid schema_download: {args.schema_download}")
436
+
437
+ return introspection_args
438
+
439
+
440
+ async def main(args: Namespace) -> int:
441
+ """Main entrypoint of the gql-cli script
442
+
443
+ :param args: The parsed command line arguments
444
+ :return: The script exit code (0 = ok, 1 = error)
445
+ """
446
+
447
+ # Set requested log level
448
+ if args.loglevel is not None:
449
+ logging.basicConfig(level=args.loglevel)
450
+
451
+ try:
452
+ # Instantiate transport from command line arguments
453
+ transport = get_transport(args)
454
+
455
+ if transport is None:
456
+ return 1
457
+
458
+ # Get extra execute parameters from command line arguments
459
+ # (variables, operation_name)
460
+ execute_args = get_execute_args(args)
461
+
462
+ except ValueError as e:
463
+ print(f"Error: {e}", file=sys.stderr)
464
+ return 1
465
+
466
+ # By default, the exit_code is 0 (everything is ok)
467
+ exit_code = 0
468
+
469
+ # Connect to the backend and provide a session
470
+ async with Client(
471
+ transport=transport,
472
+ fetch_schema_from_transport=args.print_schema,
473
+ introspection_args=get_introspection_args(args),
474
+ execute_timeout=args.execute_timeout,
475
+ ) as session:
476
+
477
+ if args.print_schema:
478
+ schema_str = print_schema(session.client.schema)
479
+ print(schema_str)
480
+
481
+ return exit_code
482
+
483
+ while True:
484
+
485
+ # Read multiple lines from input and trim whitespaces
486
+ # Will read until EOF character is received (Ctrl-D)
487
+ query_str = sys.stdin.read().strip()
488
+
489
+ # Exit if query is empty
490
+ if len(query_str) == 0:
491
+ break
492
+
493
+ # Parse query, continue on error
494
+ try:
495
+ query = gql(query_str)
496
+ except GraphQLError as e:
497
+ print(e, file=sys.stderr)
498
+ exit_code = 1
499
+ continue
500
+
501
+ # Execute or Subscribe the query depending on transport
502
+ try:
503
+ try:
504
+ async for result in session.subscribe(query, **execute_args):
505
+ print(json.dumps(result))
506
+ except KeyboardInterrupt: # pragma: no cover
507
+ pass
508
+ except NotImplementedError:
509
+ result = await session.execute(query, **execute_args)
510
+ print(json.dumps(result))
511
+ except (GraphQLError, TransportQueryError) as e:
512
+ print(e, file=sys.stderr)
513
+ exit_code = 1
514
+
515
+ return exit_code
516
+
517
+
518
+ def gql_cli() -> None:
519
+ """Synchronously invoke ``main`` with the parsed command line arguments.
520
+
521
+ Formerly ``scripts/gql-cli``, now registered as an ``entry_point``
522
+ """
523
+ # Get arguments from command line
524
+ parser = get_parser(with_examples=True)
525
+ args = parser.parse_args()
526
+
527
+ try:
528
+ # Create a new asyncio event loop
529
+ loop = asyncio.new_event_loop()
530
+ asyncio.set_event_loop(loop)
531
+
532
+ # Create a gql-cli task with the supplied arguments
533
+ main_task = asyncio.ensure_future(main(args), loop=loop)
534
+
535
+ # Add signal handlers to close gql-cli cleanly on Control-C
536
+ for signal_name in ["SIGINT", "SIGTERM", "CTRL_C_EVENT", "CTRL_BREAK_EVENT"]:
537
+ signal = getattr(signal_module, signal_name, None)
538
+
539
+ if signal is None:
540
+ continue
541
+
542
+ try:
543
+ loop.add_signal_handler(signal, main_task.cancel)
544
+ except NotImplementedError: # pragma: no cover
545
+ # not all signals supported on all platforms
546
+ pass
547
+
548
+ # Run the asyncio loop to execute the task
549
+ exit_code = 0
550
+ try:
551
+ exit_code = loop.run_until_complete(main_task)
552
+ finally:
553
+ loop.close()
554
+
555
+ # Return with the correct exit code
556
+ sys.exit(exit_code)
557
+ except KeyboardInterrupt: # pragma: no cover
558
+ pass