From 6ffc3201ec3555ba36f1c4028842ff00f2b4e8d2 Mon Sep 17 00:00:00 2001 From: "misha.gavela" Date: Sun, 3 Jun 2018 03:55:28 +0300 Subject: [PATCH 1/2] feat: added support for subscriptions --- README.md | 2 + aiohttp_graphql/graphqlview.py | 3 + aiohttp_graphql/render_graphiql.py | 131 +++-------------------------- tests/conftest.py | 4 +- tests/schema.py | 13 ++- tests/test_graphiqlview.py | 12 +++ tests/test_graphqlview.py | 51 +++++++++++ 7 files changed, 92 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index a6f2a3b..759d2eb 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Req - `encoder`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`) - `error_formatter`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`) - `enable_async`: whether `async` mode will be enabled. +- `subscriptions`: The [GraphiQL] socket endpoint for using subscriptions in [graphql-ws]. ## Testing @@ -81,3 +82,4 @@ This project is licensed under the MIT License. [Apollo-Client]: http://dev.apollodata.com/core/network.html#query-batching [Devin Fee]: https://github.com/dfee [aiohttp-graphql]: https://github.com/dfee/aiohttp-graphql + [graphql-ws]: https://github.com/graphql-python/graphql-ws diff --git a/aiohttp_graphql/graphqlview.py b/aiohttp_graphql/graphqlview.py index 362d87e..b02d317 100644 --- a/aiohttp_graphql/graphqlview.py +++ b/aiohttp_graphql/graphqlview.py @@ -36,6 +36,7 @@ def __init__( encoder=None, error_formatter=None, enable_async=True, + subscriptions=None, ): # pylint: disable=too-many-arguments # pylint: disable=too-many-locals @@ -58,6 +59,7 @@ def __init__( self.executor, AsyncioExecutor, ) + self.subscriptions = subscriptions assert isinstance(self.schema, GraphQLSchema), \ 'A Schema is required to be provided to GraphQLView.' @@ -98,6 +100,7 @@ def render_graphiql(self, params, result): result=result, graphiql_version=self.graphiql_version, graphiql_template=self.graphiql_template, + subscriptions=self.subscriptions, ) def is_graphiql(self, request): diff --git a/aiohttp_graphql/render_graphiql.py b/aiohttp_graphql/render_graphiql.py index a1eb6f5..8ee1711 100644 --- a/aiohttp_graphql/render_graphiql.py +++ b/aiohttp_graphql/render_graphiql.py @@ -1,130 +1,17 @@ +import os import json import re from aiohttp import web -GRAPHIQL_VERSION = '0.11.10' - -TEMPLATE = ''' - - - - - - - - - - - - - - -''' +with open(TEMPLATE_PATH) as template_file: + TEMPLATE = template_file.read() def escape_js_value(value): @@ -150,8 +37,8 @@ def process_var(template, name, value, jsonify=False): def simple_renderer(template, **values): - replace = ['graphiql_version'] - replace_jsonify = ['query', 'result', 'variables', 'operation_name'] + replace = ['graphiql_version', 'subscriptions', ] + replace_jsonify = ['query', 'result', 'variables', 'operation_name', ] for rep in replace: template = process_var(template, rep, values.get(rep, '')) @@ -168,6 +55,7 @@ async def render_graphiql( graphiql_template=None, params=None, result=None, + subscriptions=None, ): graphiql_version = graphiql_version or GRAPHIQL_VERSION template = graphiql_template or TEMPLATE @@ -177,6 +65,7 @@ async def render_graphiql( 'variables': params and params.variables, 'operation_name': params and params.operation_name, 'result': result, + 'subscriptions': subscriptions or '', } if jinja_env: diff --git a/tests/conftest.py b/tests/conftest.py index e765196..3384c87 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from urllib.parse import urlencode from aiohttp import web -import aiohttp.test_utils +from aiohttp.test_utils import TestClient, TestServer from graphql.execution.executors.asyncio import AsyncioExecutor import pytest @@ -34,7 +34,7 @@ def app(event_loop, executor, view_kwargs): @pytest.fixture async def client(event_loop, app): - client = aiohttp.test_utils.TestClient(app, loop=event_loop) + client = TestClient(TestServer(app), loop=event_loop) await client.start_server() yield client await client.close() diff --git a/tests/schema.py b/tests/schema.py index 07bc9ab..874ee63 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -54,7 +54,18 @@ def resolve_raises(*args): ) -Schema = GraphQLSchema(QueryRootType, MutationRootType) +SubscriptionsRootType = GraphQLObjectType( + name='SubscriptionsRoot', + fields={ + 'subscriptionsTest': GraphQLField( + type=QueryRootType, + resolver=lambda *args: QueryRootType + ) + } +) + + +Schema = GraphQLSchema(QueryRootType, MutationRootType, SubscriptionsRootType) # Schema with async methods diff --git a/tests/test_graphiqlview.py b/tests/test_graphiqlview.py index 65769dc..29fbf59 100644 --- a/tests/test_graphiqlview.py +++ b/tests/test_graphiqlview.py @@ -83,6 +83,18 @@ async def test_graphiql_get_mutation(client, url_builder): assert 'response: null' in await response.text() +@pytest.mark.asyncio +async def test_graphiql_get_subscriptions(client, url_builder): + response = await client.get( + url_builder(query=( + 'subscription TestSubscriptions { subscriptionsTest { test } }' + )), + headers={'Accept': 'text/html'}, + ) + assert response.status == 200 + assert 'response: null' in await response.text() + + class TestAsyncSchema: @pytest.fixture def executor(self, event_loop): diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 28beed2..ae6b54d 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -111,6 +111,7 @@ async def test_errors_when_missing_operation_name(client, url_builder): query=''' query TestQuery { test } mutation TestMutation { writeTest { test } } + subscription TestSubscriptions { subscriptionsTest { test } } ''' )) @@ -166,6 +167,30 @@ async def test_errors_when_selecting_a_mutation_within_a_get( ], } +@pytest.mark.asyncio +async def test_errors_when_selecting_a_subscription_within_a_get( + client, + url_builder, +): + response = await client.get(url_builder( + query=''' + subscription TestSubscriptions { subscriptionsTest { test } } + ''', + operationName='TestSubscriptions' + )) + + assert response.status == 405 + assert await response.json() == { + 'errors': [ + { + 'message': ( + 'Can only perform a subscription operation from a POST ' + 'request.' + ) + }, + ], + } + @pytest.mark.asyncio async def test_allows_mutation_to_exist_within_a_get(client, url_builder): @@ -213,6 +238,32 @@ async def test_allows_sending_a_mutation_via_post(client, base_url): } +@pytest.mark.asyncio +async def test_errors_when_sending_a_subscription_without_allow(client, base_url): + response = await client.post( + base_url, + data=json.dumps(dict( + query=''' + subscription TestSubscriptions { subscriptionsTest { test } } + ''', + )), + headers={'content-type': 'application/json'}, + ) + + assert response.status == 200 + assert await response.json() == { + 'data': None, + 'errors': [ + { + 'message': + 'Subscriptions are not allowed. You will need to ' + 'either use the subscribe function or pass ' + 'allow_subscriptions=True' + }, + ], + } + + @pytest.mark.asyncio async def test_allows_post_with_url_encoding(client, base_url): data = FormData() From 7d1fa7b4f2c9714eb0506403d149a339004ae499 Mon Sep 17 00:00:00 2001 From: "misha.gavela" Date: Sun, 3 Jun 2018 03:57:10 +0300 Subject: [PATCH 2/2] feat: added static files and corrected `.gitignore` --- .gitignore | 1 + aiohttp_graphql/static/index.jinja2 | 134 ++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 aiohttp_graphql/static/index.jinja2 diff --git a/.gitignore b/.gitignore index ad2fcc5..f6b3db0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ coverage.xml # Environments env +.idea diff --git a/aiohttp_graphql/static/index.jinja2 b/aiohttp_graphql/static/index.jinja2 new file mode 100644 index 0000000..10f8eaf --- /dev/null +++ b/aiohttp_graphql/static/index.jinja2 @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + +