Skip to content

Commit 391fc9b

Browse files
authored
Merge pull request #23 from DaleSeo/subscription
GraphQL Subscriptions Support
2 parents bb8e7b0 + 7b6f348 commit 391fc9b

File tree

4 files changed

+97
-2
lines changed

4 files changed

+97
-2
lines changed

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pip install python-graphql-client
1616

1717
## Usage
1818

19+
- Query/Mutation
20+
1921
```python
2022
from python_graphql_client import GraphqlClient
2123

@@ -45,6 +47,31 @@ data = asyncio.run(client.execute_async(query=query, variables=variables))
4547
print(data) # => {'data': {'country': {'code': 'CA', 'name': 'Canada'}}}
4648
```
4749

50+
- Subscription
51+
52+
```python
53+
from python_graphql_client import GraphqlClient
54+
55+
# Instantiate the client with a websocket endpoint.
56+
client = GraphqlClient(endpoint="wss://www.your-api.com/graphql")
57+
58+
# Create the query string and variables required for the request.
59+
query = """
60+
subscription onMessageAdded {
61+
messageAdded
62+
}
63+
"""
64+
65+
# Asynchronous request
66+
import asyncio
67+
68+
asyncio.run(client.subscribe(query=query, handle=print))
69+
# => {'data': {'messageAdded': 'Error omnis quis.'}}
70+
# => {'data': {'messageAdded': 'Enim asperiores omnis.'}}
71+
# => {'data': {'messageAdded': 'Unde ullam consequatur quam eius vel.'}}
72+
# ...
73+
```
74+
4875
## Roadmap
4976

5077
To start we'll try and use a Github project board for listing current work and updating priorities of upcoming features.

python_graphql_client/graphql_client.py

+36
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
"""Module containing graphQL client."""
2+
import json
3+
import logging
4+
from typing import Callable
25

36
import aiohttp
47
import requests
8+
import websockets
9+
10+
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
511

612

713
class GraphqlClient:
@@ -66,3 +72,33 @@ async def execute_async(
6672
headers=self.__request_headers(headers),
6773
) as response:
6874
return await response.json()
75+
76+
async def subscribe(
77+
self,
78+
query: str,
79+
handle: Callable,
80+
variables: dict = None,
81+
operation_name: str = None,
82+
headers: dict = None,
83+
):
84+
"""Make asynchronous request for GraphQL subscription."""
85+
connection_init_message = json.dumps({"type": "connection_init", "payload": {}})
86+
87+
request_body = self.__request_body(
88+
query=query, variables=variables, operation_name=operation_name
89+
)
90+
request_message = json.dumps(
91+
{"type": "start", "id": "1", "payload": request_body}
92+
)
93+
94+
async with websockets.connect(
95+
self.endpoint, subprotocols=["graphql-ws"]
96+
) as websocket:
97+
await websocket.send(connection_init_message)
98+
await websocket.send(request_message)
99+
async for response_message in websocket:
100+
response_body = json.loads(response_message)
101+
if response_body["type"] == "connection_ack":
102+
logging.info("the server accepted the connection")
103+
else:
104+
handle(response_body["payload"])

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
author_email="[email protected]",
3030
license="MIT",
3131
packages=["python_graphql_client"],
32-
install_requires=["aiohttp==3.6.2", "requests==2.22.0"],
32+
install_requires=["aiohttp==3.6.2", "requests==2.22.0", "websockets==8.1"],
3333
extras_require={
3434
"dev": [
3535
"pre-commit",

tests/test_graphql_client.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for main graphql client module."""
22

33
from unittest import IsolatedAsyncioTestCase, TestCase
4-
from unittest.mock import AsyncMock, MagicMock, patch
4+
from unittest.mock import AsyncMock, MagicMock, call, patch
55

66
from aiohttp import web
77
from requests.exceptions import HTTPError
@@ -216,3 +216,35 @@ async def test_execute_query_with_operation_name(self, mock_post):
216216
json={"query": query, "operationName": operation_name},
217217
headers={},
218218
)
219+
220+
221+
class TestGraphqlClientSubscriptions(IsolatedAsyncioTestCase):
222+
"""Test cases for subscribing GraphQL subscriptions."""
223+
224+
@patch("websockets.connect")
225+
async def test_subscribe(self, mock_connect):
226+
"""Subsribe a GraphQL subscription."""
227+
mock_websocket = mock_connect.return_value.__aenter__.return_value
228+
mock_websocket.send = AsyncMock()
229+
mock_websocket.__aiter__.return_value = [
230+
'{"type": "data", "id": "1", "payload": {"data": {"messageAdded": "one"}}}',
231+
'{"type": "data", "id": "1", "payload": {"data": {"messageAdded": "two"}}}',
232+
]
233+
234+
client = GraphqlClient(endpoint="ws://www.test-api.com/graphql")
235+
query = """
236+
subscription onMessageAdded {
237+
messageAdded
238+
}
239+
"""
240+
241+
mock_handle = MagicMock()
242+
243+
await client.subscribe(query=query, handle=mock_handle)
244+
245+
mock_handle.assert_has_calls(
246+
[
247+
call({"data": {"messageAdded": "one"}}),
248+
call({"data": {"messageAdded": "two"}}),
249+
]
250+
)

0 commit comments

Comments
 (0)