Skip to content

Commit 7cc1002

Browse files
authored
gql-cli add --transport argument (#281)
1 parent ec37cb0 commit 7cc1002

File tree

5 files changed

+316
-23
lines changed

5 files changed

+316
-23
lines changed

docs/transports/appsync.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,31 @@ a normal http session and reuse the authentication classes to create the headers
154154
Full example with API key authentication from environment variables:
155155

156156
.. literalinclude:: ../code_examples/appsync/mutation_api_key.py
157+
158+
From the command line
159+
---------------------
160+
161+
Using :ref:`gql-cli <gql_cli>`, it is possible to execute GraphQL queries and subscriptions
162+
from the command line on an AppSync endpoint.
163+
164+
- For queries and mutations, use the :code:`--transport appsync_http` argument::
165+
166+
# Put the request in a file
167+
$ echo 'mutation createMessage($message: String!) {
168+
createMessage(input: {message: $message}) {
169+
id
170+
message
171+
createdAt
172+
}
173+
}' > mutation.graphql
174+
175+
# Execute the request using gql-cli with --transport appsync_http
176+
$ cat mutation.graphql | gql-cli $AWS_GRAPHQL_API_ENDPOINT --transport appsync_http -V message:"Hello world!"
177+
178+
- For subscriptions, use the :code:`--transport appsync_websockets` argument::
179+
180+
echo "subscription{onCreateMessage{message}}" | gql-cli $AWS_GRAPHQL_API_ENDPOINT --transport appsync_websockets
181+
182+
- You can also get the full GraphQL schema from the backend from introspection::
183+
184+
$ gql-cli $AWS_GRAPHQL_API_ENDPOINT --transport appsync_http --print-schema > schema.graphql

gql/cli.py

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import sys
44
from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter
5-
from typing import Any, Dict
5+
from typing import Any, Dict, Optional
66

77
from graphql import GraphQLError, print_schema
88
from yarl import URL
@@ -101,6 +101,43 @@ def get_parser(with_examples: bool = False) -> ArgumentParser:
101101
action="store_true",
102102
dest="print_schema",
103103
)
104+
parser.add_argument(
105+
"--transport",
106+
default="auto",
107+
choices=[
108+
"auto",
109+
"aiohttp",
110+
"phoenix",
111+
"websockets",
112+
"appsync_http",
113+
"appsync_websockets",
114+
],
115+
help=(
116+
"select the transport. 'auto' by default: "
117+
"aiohttp or websockets depending on url scheme"
118+
),
119+
dest="transport",
120+
)
121+
122+
appsync_description = """
123+
By default, for an AppSync backend, the IAM authentication is chosen.
124+
125+
If you want API key or JWT authentication, you can provide one of the
126+
following arguments:"""
127+
128+
appsync_group = parser.add_argument_group(
129+
"AWS AppSync options", description=appsync_description
130+
)
131+
132+
appsync_auth_group = appsync_group.add_mutually_exclusive_group()
133+
134+
appsync_auth_group.add_argument(
135+
"--api-key", help="Provide an API key for authentication", dest="api_key",
136+
)
137+
138+
appsync_auth_group.add_argument(
139+
"--jwt", help="Provide an JSON Web token for authentication", dest="jwt",
140+
)
104141

105142
return parser
106143

@@ -191,36 +228,106 @@ def get_execute_args(args: Namespace) -> Dict[str, Any]:
191228
return execute_args
192229

193230

194-
def get_transport(args: Namespace) -> AsyncTransport:
231+
def autodetect_transport(url: URL) -> str:
232+
"""Detects which transport should be used depending on url."""
233+
234+
if url.scheme in ["ws", "wss"]:
235+
transport_name = "websockets"
236+
237+
else:
238+
assert url.scheme in ["http", "https"]
239+
transport_name = "aiohttp"
240+
241+
return transport_name
242+
243+
244+
def get_transport(args: Namespace) -> Optional[AsyncTransport]:
195245
"""Instantiate a transport from the parsed command line arguments
196246
197247
:param args: parsed command line arguments
198248
"""
199249

200250
# Get the url scheme from server parameter
201251
url = URL(args.server)
202-
scheme = url.scheme
252+
253+
# Validate scheme
254+
if url.scheme not in ["http", "https", "ws", "wss"]:
255+
raise ValueError("URL protocol should be one of: http, https, ws, wss")
203256

204257
# Get extra transport parameters from command line arguments
205258
# (headers)
206259
transport_args = get_transport_args(args)
207260

208-
# Instantiate transport depending on url scheme
209-
transport: AsyncTransport
210-
if scheme in ["ws", "wss"]:
211-
from gql.transport.websockets import WebsocketsTransport
261+
# Either use the requested transport or autodetect it
262+
if args.transport == "auto":
263+
transport_name = autodetect_transport(url)
264+
else:
265+
transport_name = args.transport
212266

213-
transport = WebsocketsTransport(
214-
url=args.server, ssl=(scheme == "wss"), **transport_args
215-
)
216-
elif scheme in ["http", "https"]:
267+
# Import the correct transport class depending on the transport name
268+
if transport_name == "aiohttp":
217269
from gql.transport.aiohttp import AIOHTTPTransport
218270

219-
transport = AIOHTTPTransport(url=args.server, **transport_args)
271+
return AIOHTTPTransport(url=args.server, **transport_args)
272+
273+
elif transport_name == "phoenix":
274+
from gql.transport.phoenix_channel_websockets import (
275+
PhoenixChannelWebsocketsTransport,
276+
)
277+
278+
return PhoenixChannelWebsocketsTransport(url=args.server, **transport_args)
279+
280+
elif transport_name == "websockets":
281+
from gql.transport.websockets import WebsocketsTransport
282+
283+
transport_args["ssl"] = url.scheme == "wss"
284+
285+
return WebsocketsTransport(url=args.server, **transport_args)
286+
220287
else:
221-
raise ValueError("URL protocol should be one of: http, https, ws, wss")
222288

223-
return transport
289+
from gql.transport.appsync_auth import AppSyncAuthentication
290+
291+
assert transport_name in ["appsync_http", "appsync_websockets"]
292+
assert url.host is not None
293+
294+
auth: AppSyncAuthentication
295+
296+
if args.api_key:
297+
from gql.transport.appsync_auth import AppSyncApiKeyAuthentication
298+
299+
auth = AppSyncApiKeyAuthentication(host=url.host, api_key=args.api_key)
300+
301+
elif args.jwt:
302+
from gql.transport.appsync_auth import AppSyncJWTAuthentication
303+
304+
auth = AppSyncJWTAuthentication(host=url.host, jwt=args.jwt)
305+
306+
else:
307+
from gql.transport.appsync_auth import AppSyncIAMAuthentication
308+
from botocore.exceptions import NoRegionError
309+
310+
try:
311+
auth = AppSyncIAMAuthentication(host=url.host)
312+
except NoRegionError:
313+
# A warning message has been printed in the console
314+
return None
315+
316+
transport_args["auth"] = auth
317+
318+
if transport_name == "appsync_http":
319+
from gql.transport.aiohttp import AIOHTTPTransport
320+
321+
return AIOHTTPTransport(url=args.server, **transport_args)
322+
323+
else:
324+
from gql.transport.appsync_websockets import AppSyncWebsocketsTransport
325+
326+
try:
327+
return AppSyncWebsocketsTransport(url=args.server, **transport_args)
328+
except Exception:
329+
# This is for the NoCredentialsError but we cannot import it here
330+
return None
224331

225332

226333
async def main(args: Namespace) -> int:
@@ -238,13 +345,16 @@ async def main(args: Namespace) -> int:
238345
# Instantiate transport from command line arguments
239346
transport = get_transport(args)
240347

348+
if transport is None:
349+
return 1
350+
241351
# Get extra execute parameters from command line arguments
242352
# (variables, operation_name)
243353
execute_args = get_execute_args(args)
244354

245355
except ValueError as e:
246356
print(f"Error: {e}", file=sys.stderr)
247-
sys.exit(1)
357+
return 1
248358

249359
# By default, the exit_code is 0 (everything is ok)
250360
exit_code = 0

gql/transport/appsync_auth.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __init__(self, host: str, api_key: str) -> None:
5454
XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.REGION.amazonaws.com
5555
:param api_key: the API key
5656
"""
57-
self._host = host
57+
self._host = host.replace("appsync-realtime-api", "appsync-api")
5858
self.api_key = api_key
5959

6060
def get_headers(
@@ -77,7 +77,7 @@ def __init__(self, host: str, jwt: str) -> None:
7777
XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.REGION.amazonaws.com
7878
:param jwt: the JWT Access Token
7979
"""
80-
self._host = host
80+
self._host = host.replace("appsync-realtime-api", "appsync-api")
8181
self.jwt = jwt
8282

8383
def get_headers(
@@ -120,7 +120,7 @@ def __init__(
120120
from botocore.awsrequest import create_request_object
121121
from botocore.session import get_session
122122

123-
self._host = host
123+
self._host = host.replace("appsync-realtime-api", "appsync-api")
124124
self._session = session if session else get_session()
125125
self._credentials = (
126126
credentials if credentials else self._session.get_credentials()
@@ -201,7 +201,7 @@ def get_headers(
201201
self._signer.add_auth(request)
202202
except NoCredentialsError:
203203
log.warning(
204-
"Credentials not found. "
204+
"Credentials not found for the IAM auth. "
205205
"Do you have default AWS credentials configured?",
206206
)
207207
raise

tests/test_aiohttp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -994,9 +994,9 @@ async def handler(request):
994994
# via the standard input
995995
monkeypatch.setattr("sys.stdin", io.StringIO(query1_str))
996996

997-
# Checking that sys.exit() is called
998-
with pytest.raises(SystemExit):
999-
await main(args)
997+
# Check that the exit_code is an error
998+
exit_code = await main(args)
999+
assert exit_code == 1
10001000

10011001
# Check that the error has been printed on stdout
10021002
captured = capsys.readouterr()

0 commit comments

Comments
 (0)