Skip to content

Commit 6d961b7

Browse files
authored
Async usage with new websockets and http async transports using asyncio (#70)
1 parent d49bd63 commit 6d961b7

31 files changed

+3697
-197
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,6 @@ target/
8989

9090
### VisualStudioCode ###
9191
.vscode/*
92+
93+
# VIM
94+
*.swp

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
env:
2+
global:
3+
- GQL_TESTS_TIMEOUT_FACTOR=100
14
language: python
25
sudo: false
36
python:
4-
- 2.7
5-
- 3.5
67
- 3.6
78
- 3.7
89
- 3.8
910
- 3.9-dev
10-
- pypy
1111
- pypy3
1212
matrix:
1313
include:

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ include Makefile
1010

1111
include tox.ini
1212

13+
include scripts/gql-cli
14+
1315
recursive-include tests *.py *.yaml *.graphql
1416
recursive-include tests_py36 *.py
1517

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,16 @@ dev-setup:
22
python pip install -e ".[test]"
33

44
tests:
5-
pytest tests --cov=gql -vv
5+
pytest tests --cov=gql -vv
6+
7+
clean:
8+
find . -name "*.pyc" -delete
9+
find . -name "__pycache__" | xargs -I {} rm -rf {}
10+
rm -rf ./htmlcov
11+
rm -rf ./.mypy_cache
12+
rm -rf ./.pytest_cache
13+
rm -rf ./.tox
14+
rm -rf ./gql.egg-info
15+
rm -rf ./dist
16+
rm -rf ./build
17+
rm -f ./.coverage

README.md

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,245 @@ query = gql('''
113113
client.execute(query)
114114
```
115115

116+
With a python version > 3.6, it is possible to execute GraphQL subscriptions using the websockets transport:
117+
118+
```python
119+
from gql import gql, Client
120+
from gql.transport.websockets import WebsocketsTransport
121+
122+
sample_transport = WebsocketsTransport(url='wss://your_server/graphql')
123+
124+
client = Client(
125+
transport=sample_transport,
126+
fetch_schema_from_transport=True,
127+
)
128+
129+
query = gql('''
130+
subscription yourSubscription {
131+
...
132+
}
133+
''')
134+
135+
for result in client.subscribe(query):
136+
print (f"result = {result!s}")
137+
```
138+
139+
Note: the websockets transport can also execute queries or mutations
140+
141+
# Async usage with asyncio
142+
143+
When using the `execute` or `subscribe` function directly on the client, the execution is synchronous.
144+
It means that we are blocked until we receive an answer from the server and
145+
we cannot do anything else while waiting for this answer.
146+
147+
It is also possible to use this library asynchronously using [asyncio](https://docs.python.org/3/library/asyncio.html).
148+
149+
Async Features:
150+
* Execute GraphQL subscriptions (See [using the websockets transport](#Websockets-async-transport))
151+
* Execute GraphQL queries, mutations and subscriptions in parallel
152+
153+
To use the async features, you need to use an async transport:
154+
* [AIOHTTPTransport](#HTTP-async-transport) for the HTTP(s) protocols
155+
* [WebsocketsTransport](#Websockets-async-transport) for the ws(s) protocols
156+
157+
## HTTP async transport
158+
159+
This transport uses the [aiohttp library](https://docs.aiohttp.org)
160+
161+
GraphQL subscriptions are not supported on the HTTP transport.
162+
For subscriptions you should use the websockets transport.
163+
164+
```python
165+
from gql import gql, Client
166+
from gql.transport.aiohttp import AIOHTTPTransport
167+
import asyncio
168+
169+
async def main():
170+
171+
sample_transport = AIOHTTPTransport(
172+
url='https://countries.trevorblades.com/graphql',
173+
headers={'Authorization': 'token'}
174+
)
175+
176+
async with Client(
177+
transport=sample_transport,
178+
fetch_schema_from_transport=True,
179+
) as session:
180+
181+
# Execute single query
182+
query = gql('''
183+
query getContinents {
184+
continents {
185+
code
186+
name
187+
}
188+
}
189+
''')
190+
191+
result = await session.execute(query)
192+
193+
print(result)
194+
195+
asyncio.run(main())
196+
```
197+
198+
## Websockets async transport
199+
200+
The websockets transport uses the apollo protocol described here:
201+
202+
[Apollo websockets transport protocol](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md)
203+
204+
This transport allows to do multiple queries, mutations and subscriptions on the same websocket connection
205+
206+
```python
207+
import logging
208+
logging.basicConfig(level=logging.INFO)
209+
210+
from gql import gql, Client
211+
from gql.transport.websockets import WebsocketsTransport
212+
import asyncio
213+
214+
async def main():
215+
216+
sample_transport = WebsocketsTransport(
217+
url='wss://countries.trevorblades.com/graphql',
218+
ssl=True,
219+
headers={'Authorization': 'token'}
220+
)
221+
222+
async with Client(
223+
transport=sample_transport,
224+
fetch_schema_from_transport=True,
225+
) as session:
226+
227+
# Execute single query
228+
query = gql('''
229+
query getContinents {
230+
continents {
231+
code
232+
name
233+
}
234+
}
235+
''')
236+
result = await session.execute(query)
237+
print(result)
238+
239+
# Request subscription
240+
subscription = gql('''
241+
subscription {
242+
somethingChanged {
243+
id
244+
}
245+
}
246+
''')
247+
async for result in session.subscribe(subscription):
248+
print(result)
249+
250+
asyncio.run(main())
251+
```
252+
253+
### Websockets SSL
254+
255+
If you need to connect to an ssl encrypted endpoint:
256+
257+
* use _wss_ instead of _ws_ in the url of the transport
258+
* set the parameter ssl to True
259+
260+
```python
261+
import ssl
262+
263+
sample_transport = WebsocketsTransport(
264+
url='wss://SERVER_URL:SERVER_PORT/graphql',
265+
headers={'Authorization': 'token'},
266+
ssl=True
267+
)
268+
```
269+
270+
If you have a self-signed ssl certificate, you need to provide an ssl_context with the server public certificate:
271+
272+
```python
273+
import pathlib
274+
import ssl
275+
276+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
277+
localhost_pem = pathlib.Path(__file__).with_name("YOUR_SERVER_PUBLIC_CERTIFICATE.pem")
278+
ssl_context.load_verify_locations(localhost_pem)
279+
280+
sample_transport = WebsocketsTransport(
281+
url='wss://SERVER_URL:SERVER_PORT/graphql',
282+
ssl=ssl_context
283+
)
284+
```
285+
286+
If you have also need to have a client ssl certificate, add:
287+
288+
```python
289+
ssl_context.load_cert_chain(certfile='YOUR_CLIENT_CERTIFICATE.pem', keyfile='YOUR_CLIENT_CERTIFICATE_KEY.key')
290+
```
291+
292+
### Websockets authentication
293+
294+
There are two ways to send authentication tokens with websockets depending on the server configuration.
295+
296+
1. Using HTTP Headers
297+
298+
```python
299+
sample_transport = WebsocketsTransport(
300+
url='wss://SERVER_URL:SERVER_PORT/graphql',
301+
headers={'Authorization': 'token'},
302+
ssl=True
303+
)
304+
```
305+
306+
2. With a payload in the connection_init websocket message
307+
308+
```python
309+
sample_transport = WebsocketsTransport(
310+
url='wss://SERVER_URL:SERVER_PORT/graphql',
311+
init_payload={'Authorization': 'token'},
312+
ssl=True
313+
)
314+
```
315+
316+
### Async advanced usage
317+
318+
It is possible to send multiple GraphQL queries (query, mutation or subscription) in parallel,
319+
on the same websocket connection, using asyncio tasks
320+
321+
```python
322+
323+
async def execute_query1():
324+
result = await session.execute(query1)
325+
print(result)
326+
327+
async def execute_query2():
328+
result = await session.execute(query2)
329+
print(result)
330+
331+
async def execute_subscription1():
332+
async for result in session.subscribe(subscription1):
333+
print(result)
334+
335+
async def execute_subscription2():
336+
async for result in session.subscribe(subscription2):
337+
print(result)
338+
339+
task1 = asyncio.create_task(execute_query1())
340+
task2 = asyncio.create_task(execute_query2())
341+
task3 = asyncio.create_task(execute_subscription1())
342+
task4 = asyncio.create_task(execute_subscription2())
343+
344+
await task1
345+
await task2
346+
await task3
347+
await task4
348+
```
349+
350+
Subscriptions tasks can be stopped at any time by running
351+
352+
```python
353+
task.cancel()
354+
```
116355

117356
## Contributing
118357
See [CONTRIBUTING.md](CONTRIBUTING.md)

gql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .gql import gql
21
from .client import Client
2+
from .gql import gql
33

44
__all__ = ["gql", "Client"]

0 commit comments

Comments
 (0)