From c116a04ca7860f11693e5f851194c1911df357d1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 13 Dec 2016 22:53:35 -0800 Subject: [PATCH 01/47] Updated READMEs --- README.md | 1 + README.rst | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3e8f3d3..c226899 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ This will add `/graphql` and `/graphiql` endpoints to your app. * `executor`: The `Executor` that you want to use to execute queries. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. + * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) You can also subclass `GraphQLView` and overwrite `get_root_value(self, request)` to have a dynamic root value per request. diff --git a/README.rst b/README.rst index 00e0852..a2c5ef7 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,9 @@ Just use the ``GraphQLView`` view from ``flask_graphql`` app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)) + # Optional, for adding batch query support (used in Apollo-Client) + app.add_url_rule('/graphql/batch', view_func=GraphQLView.as_view('graphql', schema=schema, batch=True)) + This will add ``/graphql`` and ``/graphiql`` endpoints to your app. Supported options @@ -32,8 +35,15 @@ Supported options - ``executor``: The ``Executor`` that you want to use to execute queries. - ``graphiql``: If ``True``, may present - [GraphiQL][https://github.com/graphql/graphiql] when loaded directly - from a browser (a useful tool for debugging and exploration). + `GraphiQL `__ when loaded + directly from a browser (a useful tool for debugging and + exploration). +- ``graphiql_template``: Inject a Jinja template string to customize + GraphiQL. +- ``batch``: Set the GraphQL view as batch (for using in + `Apollo-Client `__ + or + `ReactRelayNetworkLayer `__) You can also subclass ``GraphQLView`` and overwrite ``get_root_value(self, request)`` to have a dynamic root value per @@ -47,7 +57,7 @@ request. .. |Build Status| image:: https://travis-ci.org/graphql-python/flask-graphql.svg?branch=master :target: https://travis-ci.org/graphql-python/flask-graphql -.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphql-flask/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/graphql-python/graphql-flask?branch=master -.. |PyPI version| image:: https://badge.fury.io/py/graphql-flask.svg - :target: https://badge.fury.io/py/graphql-flask +.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/flask-graphql/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/graphql-python/flask-graphql?branch=master +.. |PyPI version| image:: https://badge.fury.io/py/flask-graphql.svg + :target: https://badge.fury.io/py/flask-graphql From f7a5b3644aa290065c0bdb9ceac44e71d7f7acd5 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Tue, 31 Jan 2017 17:16:10 -0500 Subject: [PATCH 02/47] Don't send referrer to jsdelivr --- flask_graphql/render_graphiql.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_graphql/render_graphiql.py b/flask_graphql/render_graphiql.py index 12d8f4c..c3a3374 100644 --- a/flask_graphql/render_graphiql.py +++ b/flask_graphql/render_graphiql.py @@ -21,6 +21,7 @@ width: 100%; } + From 5fb5cd66bb817900da84c54640802500f7812a1e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 24 Feb 2017 12:22:48 -0800 Subject: [PATCH 03/47] Refactored GraphiQL rendering --- flask_graphql/graphqlview.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 40f074b..a7f8827 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -60,6 +60,13 @@ def get_middleware(self, request): def get_executor(self, request): return self.executor + def render_graphiql(self, **kwargs): + return render_graphiql( + graphiql_version=self.graphiql_version, + graphiql_template=self.graphiql_template, + **kwargs + ): + def dispatch_request(self): try: if request.method.lower() not in ('get', 'post'): @@ -77,9 +84,7 @@ def dispatch_request(self): if show_graphiql: query, variables, operation_name, id = self.get_graphql_params(request, data) - return render_graphiql( - graphiql_version=self.graphiql_version, - graphiql_template=self.graphiql_template, + return self.render_graphiql( query=query, variables=variables, operation_name=operation_name, From 132070d59769d4edfd1520741dcd29cdd40142e4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 24 Feb 2017 13:29:26 -0800 Subject: [PATCH 04/47] Fixed tests --- flask_graphql/graphqlview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index a7f8827..a4e2515 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -65,7 +65,7 @@ def render_graphiql(self, **kwargs): graphiql_version=self.graphiql_version, graphiql_template=self.graphiql_template, **kwargs - ): + ) def dispatch_request(self): try: From 724695aaa62e3911246ac5678d78229afefe2a6f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 24 Feb 2017 13:35:36 -0800 Subject: [PATCH 05/47] Update version to 1.4.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f4d3cd7..25e8379 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='Flask-GraphQL', - version='1.4.0', + version='1.4.1', description='Adds GraphQL support to your Flask application', long_description=open('README.rst').read(), url='https://github.com/graphql-python/flask-graphql', From 3372a7b7e501d5446f1431a60f5d2d3d3c3f8463 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 15 Mar 2017 23:42:16 -0700 Subject: [PATCH 06/47] Simplified logic --- flask_graphql/graphqlview.py | 39 +++++++++++++++++------------------- tests/test_graphqlview.py | 21 ++++++++----------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index a4e2515..9f1fb79 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -73,14 +73,21 @@ def dispatch_request(self): raise HttpError(MethodNotAllowed(['GET', 'POST'], 'GraphQL only supports GET and POST requests.')) data = self.parse_body(request) + show_graphiql = self.graphiql and self.can_display_graphiql(data) - if self.batch: + if isinstance(data, list): + if not self.batch or show_graphiql: + raise HttpError(BadRequest('Batch requests are not allowed.')) + responses = [self.get_response(request, entry) for entry in data] - result = '[{}]'.format(','.join([response[0] for response in responses])) - status_code = max(responses, key=lambda response: response[1])[1] + response, status_codes = zip(*responses) + status_code = max(status_codes) else: - result, status_code = self.get_response(request, data, show_graphiql) + response, status_code = self.get_response(request, data, show_graphiql) + + pretty = self.pretty or show_graphiql or request.args.get('pretty') + result = self.json_encode(response, pretty) if show_graphiql: query, variables, operation_name, id = self.get_graphql_params(request, data) @@ -99,7 +106,7 @@ def dispatch_request(self): except HttpError as e: return Response( - self.json_encode(request, { + self.json_encode({ 'errors': [self.format_error(e)] }), status=e.response.code, @@ -132,20 +139,15 @@ def get_response(self, request, data, show_graphiql=False): response['data'] = execution_result.data if self.batch: - response = { - 'id': id, - 'payload': response, - 'status': status_code, - } + response['id'] = id - result = self.json_encode(request, response, show_graphiql) else: - result = None + response = None - return result, status_code + return response, status_code - def json_encode(self, request, d, show_graphiql=False): - pretty = self.pretty or show_graphiql or request.args.get('pretty') + @staticmethod + def json_encode(d, pretty=False): if not pretty: return json.dumps(d, separators=(',', ':')) @@ -160,12 +162,7 @@ def parse_body(self, request): elif content_type == 'application/json': try: - request_json = json.loads(request.data.decode('utf8')) - if self.batch: - assert isinstance(request_json, list) - else: - assert isinstance(request_json, dict) - return request_json + return json.loads(request.data.decode('utf8')) except: raise HttpError(BadRequest('POST body sent invalid JSON.')) diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 1efcfbc..2bd6529 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -376,12 +376,12 @@ def test_handles_errors_caused_by_a_lack_of_query(client): } -def test_handles_invalid_json_bodies(client): +def test_handles_batch_correctly_if_is_disabled(client): response = client.post(url_string(), data='[]', content_type='application/json') assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'POST body sent invalid JSON.'}] + 'errors': [{'message': 'Batch requests are not allowed.'}] } @@ -477,8 +477,7 @@ def test_batch_allows_post_with_json_encoding(client): assert response.status_code == 200 assert response_json(response) == [{ 'id': 1, - 'payload': { 'data': {'test': "Hello World"} }, - 'status': 200, + 'data': {'test': "Hello World"} }] @@ -497,8 +496,7 @@ def test_batch_supports_post_json_query_with_json_variables(client): assert response.status_code == 200 assert response_json(response) == [{ 'id': 1, - 'payload': { 'data': {'test': "Hello Dolly"} }, - 'status': 200, + 'data': {'test': "Hello Dolly"} }] @@ -524,11 +522,8 @@ def test_batch_allows_post_with_operation_name(client): assert response.status_code == 200 assert response_json(response) == [{ 'id': 1, - 'payload': { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } - }, - 'status': 200, + 'data': { + 'test': 'Hello World', + 'shared': 'Hello Everyone' + } }] From c3c9ad42602ef49f697f174e2cf68163dea8b781 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 16 Mar 2017 00:29:28 -0700 Subject: [PATCH 07/47] Abstracted HttpError exceptions --- flask_graphql/graphqlview.py | 70 +++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 9f1fb79..b7be89f 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -1,4 +1,5 @@ import json +from promise import Promise import six from flask import Response, request @@ -16,10 +17,12 @@ class HttpError(Exception): - def __init__(self, response, message=None, *args, **kwargs): - self.response = response - self.message = message = message or response.description - super(HttpError, self).__init__(message, *args, **kwargs) + def __init__(self, status_code, message=None, is_graphql_error=False, headers=None): + self.status_code = status_code + self.message = message + self.is_graphql_error = is_graphql_error + self.headers = headers + super(HttpError, self).__init__(message) class GraphQLView(View): @@ -42,7 +45,6 @@ def __init__(self, **kwargs): if hasattr(self, key): setattr(self, key, value) - assert not all((self.graphiql, self.batch)), 'Use either graphiql or batch processing' assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' # noinspection PyUnusedLocal @@ -70,27 +72,39 @@ def render_graphiql(self, **kwargs): def dispatch_request(self): try: if request.method.lower() not in ('get', 'post'): - raise HttpError(MethodNotAllowed(['GET', 'POST'], 'GraphQL only supports GET and POST requests.')) + raise HttpError( + 405, + 'GraphQL only supports GET and POST requests.', + headers={ + 'Allow': ['GET, POST'] + } + ) data = self.parse_body(request) show_graphiql = self.graphiql and self.can_display_graphiql(data) - if isinstance(data, list): + is_batch = isinstance(data, list) + if is_batch: if not self.batch or show_graphiql: - raise HttpError(BadRequest('Batch requests are not allowed.')) - - responses = [self.get_response(request, entry) for entry in data] - response, status_codes = zip(*responses) - status_code = max(status_codes) + raise HttpError( + 400, + 'Batch requests are not allowed.' + ) else: - response, status_code = self.get_response(request, data, show_graphiql) + data = [data] + responses = [self.get_response(request, entry, show_graphiql) for entry in data] + response, status_codes = zip(*responses) + status_code = max(status_codes) + + if not is_batch: + response = response[0] pretty = self.pretty or show_graphiql or request.args.get('pretty') result = self.json_encode(response, pretty) if show_graphiql: - query, variables, operation_name, id = self.get_graphql_params(request, data) + query, variables, operation_name, id = self.get_graphql_params(request, data[0]) return self.render_graphiql( query=query, variables=variables, @@ -109,14 +123,13 @@ def dispatch_request(self): self.json_encode({ 'errors': [self.format_error(e)] }), - status=e.response.code, - headers={'Allow': ['GET, POST']}, + status=e.status_code, + headers=e.headers, content_type='application/json' ) def get_response(self, request, data, show_graphiql=False): query, variables, operation_name, id = self.get_graphql_params(request, data) - execution_result = self.execute_graphql_request( data, query, @@ -124,7 +137,9 @@ def get_response(self, request, data, show_graphiql=False): operation_name, show_graphiql ) + return self.format_execution_result(execution_result, id) + def format_execution_result(self, execution_result, id): status_code = 200 if execution_result: response = {} @@ -164,7 +179,10 @@ def parse_body(self, request): try: return json.loads(request.data.decode('utf8')) except: - raise HttpError(BadRequest('POST body sent invalid JSON.')) + raise HttpError( + 400, + 'POST body sent invalid JSON.' + ) elif content_type == 'application/x-www-form-urlencoded': return request.form @@ -181,7 +199,7 @@ def execute_graphql_request(self, data, query, variables, operation_name, show_g if not query: if show_graphiql: return None - raise HttpError(BadRequest('Must provide query string.')) + raise HttpError(400, 'Must provide query string.') try: source = Source(query, name='GraphQL request') @@ -200,9 +218,13 @@ def execute_graphql_request(self, data, query, variables, operation_name, show_g if operation_ast and operation_ast.operation != 'query': if show_graphiql: return None - raise HttpError(MethodNotAllowed( - ['POST'], 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation) - )) + raise HttpError( + 405, + 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation), + headers={ + 'Allow': ['POST'], + } + ) try: return self.execute( @@ -234,13 +256,13 @@ def request_wants_html(cls, request): def get_graphql_params(request, data): query = request.args.get('query') or data.get('query') variables = request.args.get('variables') or data.get('variables') - id = request.args.get('id') or data.get('id') + id = data.get('id') if variables and isinstance(variables, six.text_type): try: variables = json.loads(variables) except: - raise HttpError(BadRequest('Variables are invalid JSON.')) + raise HttpError(400, 'Variables are invalid JSON.') operation_name = request.args.get('operationName') or data.get('operationName') From 258af89c59ce7f4ea161576bc61828d30dc3956d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 16 Mar 2017 01:26:27 -0700 Subject: [PATCH 08/47] Improved query params retreival --- flask_graphql/graphqlview.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index b7be89f..5f4d5d9 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -81,18 +81,20 @@ def dispatch_request(self): ) data = self.parse_body(request) + is_batch = isinstance(data, list) - show_graphiql = self.graphiql and self.can_display_graphiql(data) + show_graphiql = not is_batch and self.graphiql and self.can_display_graphiql(data) - is_batch = isinstance(data, list) - if is_batch: - if not self.batch or show_graphiql: - raise HttpError( - 400, - 'Batch requests are not allowed.' - ) - else: + if not is_batch: + # print data + data = dict(data, **request.args.to_dict()) data = [data] + elif not self.batch: + raise HttpError( + 400, + 'Batch requests are not allowed.' + ) + responses = [self.get_response(request, entry, show_graphiql) for entry in data] response, status_codes = zip(*responses) status_code = max(status_codes) @@ -185,10 +187,10 @@ def parse_body(self, request): ) elif content_type == 'application/x-www-form-urlencoded': - return request.form + return request.form.to_dict() elif content_type == 'multipart/form-data': - return request.form + return request.form.to_dict() return {} @@ -254,8 +256,8 @@ def request_wants_html(cls, request): @staticmethod def get_graphql_params(request, data): - query = request.args.get('query') or data.get('query') - variables = request.args.get('variables') or data.get('variables') + query = data.get('query') + variables = data.get('variables') id = data.get('id') if variables and isinstance(variables, six.text_type): @@ -264,7 +266,7 @@ def get_graphql_params(request, data): except: raise HttpError(400, 'Variables are invalid JSON.') - operation_name = request.args.get('operationName') or data.get('operationName') + operation_name = data.get('operationName') return query, variables, operation_name, id From 1b94fc8186e16e955da2911f1d9a3f0fdf36145f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 16 Mar 2017 01:38:54 -0700 Subject: [PATCH 09/47] Improved JSON dump --- flask_graphql/graphqlview.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 5f4d5d9..e9c2921 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -164,12 +164,15 @@ def format_execution_result(self, execution_result, id): return response, status_code @staticmethod - def json_encode(d, pretty=False): + def json_encode(data, pretty=False): if not pretty: - return json.dumps(d, separators=(',', ':')) + return json.dumps(data, separators=(',', ':')) - return json.dumps(d, sort_keys=True, - indent=2, separators=(',', ': ')) + return json.dumps( + data, + indent=2, + separators=(',', ': ') + ) # noinspection PyBroadException def parse_body(self, request): From d3e6fc6cd101a68ab9265c14cff2ea6f18dec69c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 16 Mar 2017 01:43:20 -0700 Subject: [PATCH 10/47] Simplified logic away from request --- flask_graphql/graphqlview.py | 117 ++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 49 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index e9c2921..54ec524 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -71,7 +71,8 @@ def render_graphiql(self, **kwargs): def dispatch_request(self): try: - if request.method.lower() not in ('get', 'post'): + request_method = request.method.lower() + if request_method not in ('get', 'post'): raise HttpError( 405, 'GraphQL only supports GET and POST requests.', @@ -95,7 +96,15 @@ def dispatch_request(self): 'Batch requests are not allowed.' ) - responses = [self.get_response(request, entry, show_graphiql) for entry in data] + only_allow_query = request_method == 'get' + + responses = [self.get_response( + self.execute, + entry, + show_graphiql, + only_allow_query, + ) for entry in data] + response, status_codes = zip(*responses) status_code = max(status_codes) @@ -106,7 +115,7 @@ def dispatch_request(self): result = self.json_encode(response, pretty) if show_graphiql: - query, variables, operation_name, id = self.get_graphql_params(request, data[0]) + query, variables, operation_name, id = self.get_graphql_params(data[0]) return self.render_graphiql( query=query, variables=variables, @@ -130,15 +139,23 @@ def dispatch_request(self): content_type='application/json' ) - def get_response(self, request, data, show_graphiql=False): - query, variables, operation_name, id = self.get_graphql_params(request, data) - execution_result = self.execute_graphql_request( - data, - query, - variables, - operation_name, - show_graphiql - ) + def get_response(self, execute, data, show_graphiql=False, only_allow_query=False): + query, variables, operation_name, id = self.get_graphql_params(data) + try: + execution_result = self.execute_graphql_request( + self.schema, + execute, + data, + query, + variables, + operation_name, + only_allow_query, + ) + except HttpError: + if show_graphiql: + execution_result = None + else: + raise return self.format_execution_result(execution_result, id) def format_execution_result(self, execution_result, id): @@ -163,20 +180,11 @@ def format_execution_result(self, execution_result, id): return response, status_code - @staticmethod - def json_encode(data, pretty=False): - if not pretty: - return json.dumps(data, separators=(',', ':')) - - return json.dumps( - data, - indent=2, - separators=(',', ': ') - ) - # noinspection PyBroadException def parse_body(self, request): - content_type = self.get_content_type(request) + # We use mimetype here since we don't need the other + # information provided by content_type + content_type = request.mimetype if content_type == 'application/graphql': return {'query': request.data.decode()} @@ -197,19 +205,31 @@ def parse_body(self, request): return {} - def execute(self, *args, **kwargs): - return execute(self.schema, *args, **kwargs) + def execute(self, schema, *args, **kwargs): + root_value = self.get_root_value(request) + context_value = self.get_context(request) + middleware = self.get_middleware(request) + executor = self.get_executor(request) + + return execute( + schema, + *args, + root_value=root_value, + context_value=context_value, + middleware=middleware, + executor=executor, + **kwargs + ) - def execute_graphql_request(self, data, query, variables, operation_name, show_graphiql=False): + @staticmethod + def execute_graphql_request(schema, execute, data, query, variables, operation_name, only_allow_query=False): if not query: - if show_graphiql: - return None raise HttpError(400, 'Must provide query string.') try: source = Source(query, name='GraphQL request') ast = parse(source) - validation_errors = validate(self.schema, ast) + validation_errors = validate(schema, ast) if validation_errors: return ExecutionResult( errors=validation_errors, @@ -218,11 +238,9 @@ def execute_graphql_request(self, data, query, variables, operation_name, show_g except Exception as e: return ExecutionResult(errors=[e], invalid=True) - if request.method.lower() == 'get': + if only_allow_query: operation_ast = get_operation_ast(ast, operation_name) if operation_ast and operation_ast.operation != 'query': - if show_graphiql: - return None raise HttpError( 405, 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation), @@ -232,25 +250,32 @@ def execute_graphql_request(self, data, query, variables, operation_name, show_g ) try: - return self.execute( + return execute( + schema, ast, - root_value=self.get_root_value(request), - variable_values=variables or {}, operation_name=operation_name, - context_value=self.get_context(request), - middleware=self.get_middleware(request), - executor=self.get_executor(request) + variable_values=variables, ) except Exception as e: return ExecutionResult(errors=[e], invalid=True) + @staticmethod + def json_encode(data, pretty=False): + if not pretty: + return json.dumps(data, separators=(',', ':')) + + return json.dumps( + data, + indent=2, + separators=(',', ': ') + ) + @classmethod def can_display_graphiql(cls, data): - raw = 'raw' in request.args or 'raw' in data - return not raw and cls.request_wants_html(request) + return 'raw' not in data and cls.request_wants_html() @classmethod - def request_wants_html(cls, request): + def request_wants_html(cls): best = request.accept_mimetypes \ .best_match(['application/json', 'text/html']) return best == 'text/html' and \ @@ -258,7 +283,7 @@ def request_wants_html(cls, request): request.accept_mimetypes['application/json'] @staticmethod - def get_graphql_params(request, data): + def get_graphql_params(data): query = data.get('query') variables = data.get('variables') id = data.get('id') @@ -279,9 +304,3 @@ def format_error(error): return format_graphql_error(error) return {'message': six.text_type(error)} - - @staticmethod - def get_content_type(request): - # We use mimetype here since we don't need the other - # information provided by content_type - return request.mimetype From 9e206b2039e90b71781de438af1b4839f724f8f0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 12:11:17 -0700 Subject: [PATCH 11/47] Isolated format error --- flask_graphql/graphqlview.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 54ec524..9b703b8 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -87,7 +87,7 @@ def dispatch_request(self): show_graphiql = not is_batch and self.graphiql and self.can_display_graphiql(data) if not is_batch: - # print data + assert isinstance(data, dict), "GraphQL params should be a dict. Received {}.".format(data) data = dict(data, **request.args.to_dict()) data = [data] elif not self.batch: @@ -156,15 +156,16 @@ def get_response(self, execute, data, show_graphiql=False, only_allow_query=Fals execution_result = None else: raise - return self.format_execution_result(execution_result, id) + return self.format_execution_result(execution_result, id, self.format_error) - def format_execution_result(self, execution_result, id): + @staticmethod + def format_execution_result(execution_result, id, format_error): status_code = 200 if execution_result: response = {} if execution_result.errors: - response['errors'] = [self.format_error(e) for e in execution_result.errors] + response['errors'] = [format_error(e) for e in execution_result.errors] if execution_result.invalid: status_code = 400 @@ -172,7 +173,7 @@ def format_execution_result(self, execution_result, id): status_code = 200 response['data'] = execution_result.data - if self.batch: + if id: response['id'] = id else: From 75b1c29dc36956d16e24620a56dbcc8838b691f5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 13:20:07 -0700 Subject: [PATCH 12/47] Improved GraphiQL logic --- flask_graphql/graphqlview.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 9b703b8..9c11663 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -84,7 +84,7 @@ def dispatch_request(self): data = self.parse_body(request) is_batch = isinstance(data, list) - show_graphiql = not is_batch and self.graphiql and self.can_display_graphiql(data) + show_graphiql = not is_batch and self.should_display_graphiql(data) if not is_batch: assert isinstance(data, dict), "GraphQL params should be a dict. Received {}.".format(data) @@ -271,12 +271,13 @@ def json_encode(data, pretty=False): separators=(',', ': ') ) - @classmethod - def can_display_graphiql(cls, data): - return 'raw' not in data and cls.request_wants_html() + def should_display_graphiql(self, data): + if not self.graphiql or 'raw' in data: + return False - @classmethod - def request_wants_html(cls): + return self.request_wants_html() + + def request_wants_html(self): best = request.accept_mimetypes \ .best_match(['application/json', 'text/html']) return best == 'text/html' and \ From 3050e0ad65015d7540fad9964588a4c69448421e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 16:01:13 -0700 Subject: [PATCH 13/47] Renamed HttpError to HttpQueryError --- flask_graphql/graphqlview.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 9c11663..c288d1e 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -4,7 +4,6 @@ import six from flask import Response, request from flask.views import View -from werkzeug.exceptions import BadRequest, MethodNotAllowed from graphql import Source, execute, parse, validate from graphql.error import format_error as format_graphql_error @@ -16,13 +15,13 @@ from .render_graphiql import render_graphiql -class HttpError(Exception): +class HttpQueryError(Exception): def __init__(self, status_code, message=None, is_graphql_error=False, headers=None): self.status_code = status_code self.message = message self.is_graphql_error = is_graphql_error self.headers = headers - super(HttpError, self).__init__(message) + super(HttpQueryError, self).__init__(message) class GraphQLView(View): @@ -70,10 +69,11 @@ def render_graphiql(self, **kwargs): ) def dispatch_request(self): + try: request_method = request.method.lower() if request_method not in ('get', 'post'): - raise HttpError( + raise HttpQueryError( 405, 'GraphQL only supports GET and POST requests.', headers={ @@ -91,7 +91,7 @@ def dispatch_request(self): data = dict(data, **request.args.to_dict()) data = [data] elif not self.batch: - raise HttpError( + raise HttpQueryError( 400, 'Batch requests are not allowed.' ) @@ -129,7 +129,7 @@ def dispatch_request(self): content_type='application/json' ) - except HttpError as e: + except HttpQueryError as e: return Response( self.json_encode({ 'errors': [self.format_error(e)] @@ -151,7 +151,7 @@ def get_response(self, execute, data, show_graphiql=False, only_allow_query=Fals operation_name, only_allow_query, ) - except HttpError: + except HttpQueryError: if show_graphiql: execution_result = None else: @@ -193,7 +193,7 @@ def parse_body(self, request): try: return json.loads(request.data.decode('utf8')) except: - raise HttpError( + raise HttpQueryError( 400, 'POST body sent invalid JSON.' ) @@ -225,7 +225,7 @@ def execute(self, schema, *args, **kwargs): @staticmethod def execute_graphql_request(schema, execute, data, query, variables, operation_name, only_allow_query=False): if not query: - raise HttpError(400, 'Must provide query string.') + raise HttpQueryError(400, 'Must provide query string.') try: source = Source(query, name='GraphQL request') @@ -242,7 +242,7 @@ def execute_graphql_request(schema, execute, data, query, variables, operation_n if only_allow_query: operation_ast = get_operation_ast(ast, operation_name) if operation_ast and operation_ast.operation != 'query': - raise HttpError( + raise HttpQueryError( 405, 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation), headers={ @@ -294,7 +294,7 @@ def get_graphql_params(data): try: variables = json.loads(variables) except: - raise HttpError(400, 'Variables are invalid JSON.') + raise HttpQueryError(400, 'Variables are invalid JSON.') operation_name = data.get('operationName') From de3313c35faa87833d9c6af201fa22c62df27afa Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 17:21:49 -0700 Subject: [PATCH 14/47] Refactored params --- flask_graphql/graphqlview.py | 50 +++++++++++++++++--------------- flask_graphql/render_graphiql.py | 14 +++++---- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index c288d1e..75a2160 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -1,5 +1,6 @@ import json from promise import Promise +from collections import namedtuple import six from flask import Response, request @@ -15,6 +16,8 @@ from .render_graphiql import render_graphiql +GraphQLParams = namedtuple('GraphQLParams', 'query,variables,operation_name,id') + class HttpQueryError(Exception): def __init__(self, status_code, message=None, is_graphql_error=False, headers=None): self.status_code = status_code @@ -61,11 +64,12 @@ def get_middleware(self, request): def get_executor(self, request): return self.executor - def render_graphiql(self, **kwargs): + def render_graphiql(self, params, result): return render_graphiql( + params=params, + result=result, graphiql_version=self.graphiql_version, graphiql_template=self.graphiql_template, - **kwargs ) def dispatch_request(self): @@ -115,11 +119,9 @@ def dispatch_request(self): result = self.json_encode(response, pretty) if show_graphiql: - query, variables, operation_name, id = self.get_graphql_params(data[0]) + params = self.get_graphql_params(data[0]) return self.render_graphiql( - query=query, - variables=variables, - operation_name=operation_name, + params=params, result=result ) @@ -140,15 +142,13 @@ def dispatch_request(self): ) def get_response(self, execute, data, show_graphiql=False, only_allow_query=False): - query, variables, operation_name, id = self.get_graphql_params(data) + params = self.get_graphql_params(data) try: execution_result = self.execute_graphql_request( self.schema, execute, data, - query, - variables, - operation_name, + params, only_allow_query, ) except HttpQueryError: @@ -156,7 +156,7 @@ def get_response(self, execute, data, show_graphiql=False, only_allow_query=Fals execution_result = None else: raise - return self.format_execution_result(execution_result, id, self.format_error) + return self.format_execution_result(execution_result, params.id, self.format_error) @staticmethod def format_execution_result(execution_result, id, format_error): @@ -223,12 +223,12 @@ def execute(self, schema, *args, **kwargs): ) @staticmethod - def execute_graphql_request(schema, execute, data, query, variables, operation_name, only_allow_query=False): - if not query: + def execute_graphql_request(schema, execute, data, params, only_allow_query=False): + if not params.query: raise HttpQueryError(400, 'Must provide query string.') try: - source = Source(query, name='GraphQL request') + source = Source(params.query, name='GraphQL request') ast = parse(source) validation_errors = validate(schema, ast) if validation_errors: @@ -240,7 +240,7 @@ def execute_graphql_request(schema, execute, data, query, variables, operation_n return ExecutionResult(errors=[e], invalid=True) if only_allow_query: - operation_ast = get_operation_ast(ast, operation_name) + operation_ast = get_operation_ast(ast, params.operation_name) if operation_ast and operation_ast.operation != 'query': raise HttpQueryError( 405, @@ -254,8 +254,8 @@ def execute_graphql_request(schema, execute, data, query, variables, operation_n return execute( schema, ast, - operation_name=operation_name, - variable_values=variables, + operation_name=params.operation_name, + variable_values=params.variables, ) except Exception as e: return ExecutionResult(errors=[e], invalid=True) @@ -285,20 +285,22 @@ def request_wants_html(self): request.accept_mimetypes['application/json'] @staticmethod - def get_graphql_params(data): - query = data.get('query') - variables = data.get('variables') - id = data.get('id') - + def get_variables(variables): if variables and isinstance(variables, six.text_type): try: - variables = json.loads(variables) + return json.loads(variables) except: raise HttpQueryError(400, 'Variables are invalid JSON.') + return variables + @classmethod + def get_graphql_params(cls, data): + query = data.get('query') + variables = cls.get_variables(data.get('variables')) + id = data.get('id') operation_name = data.get('operationName') - return query, variables, operation_name, id + return GraphQLParams(query, variables, operation_name, id) @staticmethod def format_error(error): diff --git a/flask_graphql/render_graphiql.py b/flask_graphql/render_graphiql.py index c3a3374..bd2f714 100644 --- a/flask_graphql/render_graphiql.py +++ b/flask_graphql/render_graphiql.py @@ -112,10 +112,10 @@ onEditQuery: onEditQuery, onEditVariables: onEditVariables, onEditOperationName: onEditOperationName, - query: {{ query|tojson }}, + query: {{ params.query|tojson }}, response: {{ result|tojson }}, - variables: {{ variables|tojson }}, - operationName: {{ operation_name|tojson }}, + variables: {{ params.variables|tojson }}, + operationName: {{ params.operation_name|tojson }}, }), document.body ); @@ -124,9 +124,13 @@ ''' -def render_graphiql(graphiql_version=None, graphiql_template=None, **kwargs): +def render_graphiql(params, result, graphiql_version=None, graphiql_template=None): graphiql_version = graphiql_version or GRAPHIQL_VERSION template = graphiql_template or TEMPLATE return render_template_string( - template, graphiql_version=graphiql_version, **kwargs) + template, + graphiql_version=graphiql_version, + result=result, + params=params + ) From fab7f0431148c737d3874e73ee45225c0b2c8a2b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 17:24:53 -0700 Subject: [PATCH 15/47] Simplified response catch logic --- flask_graphql/graphqlview.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 75a2160..29424ab 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -89,6 +89,7 @@ def dispatch_request(self): is_batch = isinstance(data, list) show_graphiql = not is_batch and self.should_display_graphiql(data) + catch = HttpQueryError if show_graphiql else None if not is_batch: assert isinstance(data, dict), "GraphQL params should be a dict. Received {}.".format(data) @@ -105,7 +106,7 @@ def dispatch_request(self): responses = [self.get_response( self.execute, entry, - show_graphiql, + catch, only_allow_query, ) for entry in data] @@ -141,7 +142,7 @@ def dispatch_request(self): content_type='application/json' ) - def get_response(self, execute, data, show_graphiql=False, only_allow_query=False): + def get_response(self, execute, data, catch=None, only_allow_query=False): params = self.get_graphql_params(data) try: execution_result = self.execute_graphql_request( @@ -151,11 +152,8 @@ def get_response(self, execute, data, show_graphiql=False, only_allow_query=Fals params, only_allow_query, ) - except HttpQueryError: - if show_graphiql: - execution_result = None - else: - raise + except catch: + execution_result = None return self.format_execution_result(execution_result, params.id, self.format_error) @staticmethod From 31fcca57f5996890d86bdb6fc6db8045d2eb332a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 17:32:58 -0700 Subject: [PATCH 16/47] Added GraphQLResponse structure --- flask_graphql/graphqlview.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 29424ab..57216c7 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -17,6 +17,8 @@ GraphQLParams = namedtuple('GraphQLParams', 'query,variables,operation_name,id') +GraphQLResponse = namedtuple('GraphQLResponse', 'result,params,status_code') + class HttpQueryError(Exception): def __init__(self, status_code, message=None, is_graphql_error=False, headers=None): @@ -85,7 +87,7 @@ def dispatch_request(self): } ) - data = self.parse_body(request) + data = self.parse_body() is_batch = isinstance(data, list) show_graphiql = not is_batch and self.should_display_graphiql(data) @@ -110,7 +112,7 @@ def dispatch_request(self): only_allow_query, ) for entry in data] - response, status_codes = zip(*responses) + response, params, status_codes = zip(*responses) status_code = max(status_codes) if not is_batch: @@ -120,9 +122,8 @@ def dispatch_request(self): result = self.json_encode(response, pretty) if show_graphiql: - params = self.get_graphql_params(data[0]) return self.render_graphiql( - params=params, + params=params[0], result=result ) @@ -154,7 +155,13 @@ def get_response(self, execute, data, catch=None, only_allow_query=False): ) except catch: execution_result = None - return self.format_execution_result(execution_result, params.id, self.format_error) + + response, status_code = self.format_execution_result(execution_result, params.id, self.format_error) + return GraphQLResponse( + response, + params, + status_code + ) @staticmethod def format_execution_result(execution_result, id, format_error): @@ -180,7 +187,7 @@ def format_execution_result(execution_result, id, format_error): return response, status_code # noinspection PyBroadException - def parse_body(self, request): + def parse_body(self): # We use mimetype here since we don't need the other # information provided by content_type content_type = request.mimetype From 69f7c2608740b48e8baddca2401fbf7839ac498f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 17:39:38 -0700 Subject: [PATCH 17/47] Simplified execution --- flask_graphql/graphqlview.py | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 57216c7..2ae83fc 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -92,6 +92,7 @@ def dispatch_request(self): show_graphiql = not is_batch and self.should_display_graphiql(data) catch = HttpQueryError if show_graphiql else None + only_allow_query = request_method == 'get' if not is_batch: assert isinstance(data, dict), "GraphQL params should be a dict. Received {}.".format(data) @@ -103,13 +104,16 @@ def dispatch_request(self): 'Batch requests are not allowed.' ) - only_allow_query = request_method == 'get' responses = [self.get_response( - self.execute, + self.schema, entry, catch, only_allow_query, + root_value=self.get_root_value(request), + context_value=self.get_context(request), + middleware=self.get_middleware(request), + executor=self.get_executor(request), ) for entry in data] response, params, status_codes = zip(*responses) @@ -143,15 +147,15 @@ def dispatch_request(self): content_type='application/json' ) - def get_response(self, execute, data, catch=None, only_allow_query=False): + def get_response(self, schema, data, catch=None, only_allow_query=False, **kwargs): params = self.get_graphql_params(data) try: execution_result = self.execute_graphql_request( - self.schema, - execute, + schema, data, params, only_allow_query, + **kwargs ) except catch: execution_result = None @@ -211,24 +215,8 @@ def parse_body(self): return {} - def execute(self, schema, *args, **kwargs): - root_value = self.get_root_value(request) - context_value = self.get_context(request) - middleware = self.get_middleware(request) - executor = self.get_executor(request) - - return execute( - schema, - *args, - root_value=root_value, - context_value=context_value, - middleware=middleware, - executor=executor, - **kwargs - ) - @staticmethod - def execute_graphql_request(schema, execute, data, params, only_allow_query=False): + def execute_graphql_request(schema, data, params, only_allow_query=False, **kwargs): if not params.query: raise HttpQueryError(400, 'Must provide query string.') @@ -261,6 +249,7 @@ def execute_graphql_request(schema, execute, data, params, only_allow_query=Fals ast, operation_name=params.operation_name, variable_values=params.variables, + **kwargs ) except Exception as e: return ExecutionResult(errors=[e], invalid=True) From 6a992382080cfed2af569de2ff513f76d2c92794 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 17:46:32 -0700 Subject: [PATCH 18/47] Improved batch error message --- flask_graphql/graphqlview.py | 2 +- tests/test_graphqlview.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 2ae83fc..73a5846 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -101,7 +101,7 @@ def dispatch_request(self): elif not self.batch: raise HttpQueryError( 400, - 'Batch requests are not allowed.' + 'Batch GraphQL requests are not enabled.' ) diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 2bd6529..9697358 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -381,7 +381,7 @@ def test_handles_batch_correctly_if_is_disabled(client): assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Batch requests are not allowed.'}] + 'errors': [{'message': 'Batch GraphQL requests are not enabled.'}] } From 330a59fa9539f4b2d4446cb19e15ace5683187f6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 17:52:22 -0700 Subject: [PATCH 19/47] Removed unused data --- flask_graphql/graphqlview.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 73a5846..f51e153 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -107,7 +107,7 @@ def dispatch_request(self): responses = [self.get_response( self.schema, - entry, + self.get_graphql_params(entry), catch, only_allow_query, root_value=self.get_root_value(request), @@ -147,12 +147,10 @@ def dispatch_request(self): content_type='application/json' ) - def get_response(self, schema, data, catch=None, only_allow_query=False, **kwargs): - params = self.get_graphql_params(data) + def get_response(self, schema, params, catch=None, only_allow_query=False, **kwargs): try: execution_result = self.execute_graphql_request( schema, - data, params, only_allow_query, **kwargs @@ -216,7 +214,7 @@ def parse_body(self): return {} @staticmethod - def execute_graphql_request(schema, data, params, only_allow_query=False, **kwargs): + def execute_graphql_request(schema, params, only_allow_query=False, **kwargs): if not params.query: raise HttpQueryError(400, 'Must provide query string.') From 9cbe59bab41048dbc1484d45da0bf72d996324af Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 17:56:29 -0700 Subject: [PATCH 20/47] Fixed assertion errors --- flask_graphql/graphqlview.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index f51e153..8740738 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -95,7 +95,11 @@ def dispatch_request(self): only_allow_query = request_method == 'get' if not is_batch: - assert isinstance(data, dict), "GraphQL params should be a dict. Received {}.".format(data) + if not isinstance(data, dict): + raise HttpQueryError( + 400, + 'GraphQL params should be a dict. Received {}.'.format(data) + ) data = dict(data, **request.args.to_dict()) data = [data] elif not self.batch: From 182782cfe83230436beeebf3c3a0c03399927762 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 18:03:39 -0700 Subject: [PATCH 21/47] Removed params from response --- flask_graphql/graphqlview.py | 45 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 8740738..737bf6b 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -17,7 +17,7 @@ GraphQLParams = namedtuple('GraphQLParams', 'query,variables,operation_name,id') -GraphQLResponse = namedtuple('GraphQLResponse', 'result,params,status_code') +GraphQLResponse = namedtuple('GraphQLResponse', 'result,status_code') class HttpQueryError(Exception): @@ -51,19 +51,23 @@ def __init__(self, **kwargs): assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' + # Flask # noinspection PyUnusedLocal - def get_root_value(self, request): + def get_root_value(self): return self.root_value - def get_context(self, request): + # Flask + def get_context(self): if self.context is not None: return self.context return request - def get_middleware(self, request): + # Flask + def get_middleware(self): return self.middleware - def get_executor(self, request): + # Flask + def get_executor(self): return self.executor def render_graphiql(self, params, result): @@ -108,19 +112,20 @@ def dispatch_request(self): 'Batch GraphQL requests are not enabled.' ) + all_params = [self.get_graphql_params(entry) for entry in data] responses = [self.get_response( self.schema, - self.get_graphql_params(entry), + params, catch, only_allow_query, - root_value=self.get_root_value(request), - context_value=self.get_context(request), - middleware=self.get_middleware(request), - executor=self.get_executor(request), - ) for entry in data] + root_value=self.get_root_value(), + context_value=self.get_context(), + middleware=self.get_middleware(), + executor=self.get_executor(), + ) for params in all_params] - response, params, status_codes = zip(*responses) + response, status_codes = zip(*responses) status_code = max(status_codes) if not is_batch: @@ -131,7 +136,7 @@ def dispatch_request(self): if show_graphiql: return self.render_graphiql( - params=params[0], + params=all_params[0], result=result ) @@ -160,14 +165,9 @@ def get_response(self, schema, params, catch=None, only_allow_query=False, **kwa **kwargs ) except catch: - execution_result = None + return GraphQLResponse(None, 400) - response, status_code = self.format_execution_result(execution_result, params.id, self.format_error) - return GraphQLResponse( - response, - params, - status_code - ) + return self.format_execution_result(execution_result, params.id, self.format_error) @staticmethod def format_execution_result(execution_result, id, format_error): @@ -190,8 +190,9 @@ def format_execution_result(execution_result, id, format_error): else: response = None - return response, status_code + return GraphQLResponse(response, status_code) + # Flask # noinspection PyBroadException def parse_body(self): # We use mimetype here since we don't need the other @@ -267,12 +268,14 @@ def json_encode(data, pretty=False): separators=(',', ': ') ) + # Flask def should_display_graphiql(self, data): if not self.graphiql or 'raw' in data: return False return self.request_wants_html() + # Flask def request_wants_html(self): best = request.accept_mimetypes \ .best_match(['application/json', 'text/html']) From 05a6077f40063b9637d69cc0d640537e17e7ce20 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Mar 2017 20:18:13 -0700 Subject: [PATCH 22/47] Refactored code using graphql_server --- flask_graphql/graphqlview.py | 201 ++++------------------------------- tests/app.py | 2 +- 2 files changed, 21 insertions(+), 182 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 737bf6b..1c438e5 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -1,34 +1,14 @@ import json -from promise import Promise -from collections import namedtuple -import six from flask import Response, request from flask.views import View -from graphql import Source, execute, parse, validate -from graphql.error import format_error as format_graphql_error -from graphql.error import GraphQLError -from graphql.execution import ExecutionResult from graphql.type.schema import GraphQLSchema -from graphql.utils.get_operation_ast import get_operation_ast +from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body from .render_graphiql import render_graphiql -GraphQLParams = namedtuple('GraphQLParams', 'query,variables,operation_name,id') -GraphQLResponse = namedtuple('GraphQLResponse', 'result,status_code') - - -class HttpQueryError(Exception): - def __init__(self, status_code, message=None, is_graphql_error=False, headers=None): - self.status_code = status_code - self.message = message - self.is_graphql_error = is_graphql_error - self.headers = headers - super(HttpQueryError, self).__init__(message) - - class GraphQLView(View): schema = None executor = None @@ -51,22 +31,18 @@ def __init__(self, **kwargs): assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' - # Flask # noinspection PyUnusedLocal def get_root_value(self): return self.root_value - # Flask def get_context(self): if self.context is not None: return self.context return request - # Flask def get_middleware(self): return self.middleware - # Flask def get_executor(self): return self.executor @@ -82,57 +58,30 @@ def dispatch_request(self): try: request_method = request.method.lower() - if request_method not in ('get', 'post'): - raise HttpQueryError( - 405, - 'GraphQL only supports GET and POST requests.', - headers={ - 'Allow': ['GET, POST'] - } - ) - data = self.parse_body() - is_batch = isinstance(data, list) + if isinstance(data, dict): + data = dict(data, **request.args.to_dict()) - show_graphiql = not is_batch and self.should_display_graphiql(data) + show_graphiql = request_method == 'get' and self.should_display_graphiql() catch = HttpQueryError if show_graphiql else None - only_allow_query = request_method == 'get' - - if not is_batch: - if not isinstance(data, dict): - raise HttpQueryError( - 400, - 'GraphQL params should be a dict. Received {}.'.format(data) - ) - data = dict(data, **request.args.to_dict()) - data = [data] - elif not self.batch: - raise HttpQueryError( - 400, - 'Batch GraphQL requests are not enabled.' - ) - all_params = [self.get_graphql_params(entry) for entry in data] + pretty = self.pretty or show_graphiql or request.args.get('pretty') - responses = [self.get_response( + result, status_code, all_params = run_http_query( self.schema, - params, - catch, - only_allow_query, + request_method, + data, + batch_enabled=self.batch, + catch=catch, + + # Execute options root_value=self.get_root_value(), context_value=self.get_context(), middleware=self.get_middleware(), executor=self.get_executor(), - ) for params in all_params] - - response, status_codes = zip(*responses) - status_code = max(status_codes) - - if not is_batch: - response = response[0] + ) - pretty = self.pretty or show_graphiql or request.args.get('pretty') - result = self.json_encode(response, pretty) + result = self.json_encode(result, pretty) if show_graphiql: return self.render_graphiql( @@ -149,49 +98,13 @@ def dispatch_request(self): except HttpQueryError as e: return Response( self.json_encode({ - 'errors': [self.format_error(e)] + 'errors': [default_format_error(e)] }), status=e.status_code, headers=e.headers, content_type='application/json' ) - def get_response(self, schema, params, catch=None, only_allow_query=False, **kwargs): - try: - execution_result = self.execute_graphql_request( - schema, - params, - only_allow_query, - **kwargs - ) - except catch: - return GraphQLResponse(None, 400) - - return self.format_execution_result(execution_result, params.id, self.format_error) - - @staticmethod - def format_execution_result(execution_result, id, format_error): - status_code = 200 - if execution_result: - response = {} - - if execution_result.errors: - response['errors'] = [format_error(e) for e in execution_result.errors] - - if execution_result.invalid: - status_code = 400 - else: - status_code = 200 - response['data'] = execution_result.data - - if id: - response['id'] = id - - else: - response = None - - return GraphQLResponse(response, status_code) - # Flask # noinspection PyBroadException def parse_body(self): @@ -202,61 +115,14 @@ def parse_body(self): return {'query': request.data.decode()} elif content_type == 'application/json': - try: - return json.loads(request.data.decode('utf8')) - except: - raise HttpQueryError( - 400, - 'POST body sent invalid JSON.' - ) - - elif content_type == 'application/x-www-form-urlencoded': - return request.form.to_dict() + return load_json_body(request.data.decode('utf8')) - elif content_type == 'multipart/form-data': + elif content_type == 'application/x-www-form-urlencoded' \ + or content_type == 'multipart/form-data': return request.form.to_dict() return {} - @staticmethod - def execute_graphql_request(schema, params, only_allow_query=False, **kwargs): - if not params.query: - raise HttpQueryError(400, 'Must provide query string.') - - try: - source = Source(params.query, name='GraphQL request') - ast = parse(source) - validation_errors = validate(schema, ast) - if validation_errors: - return ExecutionResult( - errors=validation_errors, - invalid=True, - ) - except Exception as e: - return ExecutionResult(errors=[e], invalid=True) - - if only_allow_query: - operation_ast = get_operation_ast(ast, params.operation_name) - if operation_ast and operation_ast.operation != 'query': - raise HttpQueryError( - 405, - 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation), - headers={ - 'Allow': ['POST'], - } - ) - - try: - return execute( - schema, - ast, - operation_name=params.operation_name, - variable_values=params.variables, - **kwargs - ) - except Exception as e: - return ExecutionResult(errors=[e], invalid=True) - @staticmethod def json_encode(data, pretty=False): if not pretty: @@ -268,42 +134,15 @@ def json_encode(data, pretty=False): separators=(',', ': ') ) - # Flask - def should_display_graphiql(self, data): - if not self.graphiql or 'raw' in data: + def should_display_graphiql(self): + if not self.graphiql or 'raw' in request.args: return False return self.request_wants_html() - # Flask def request_wants_html(self): best = request.accept_mimetypes \ .best_match(['application/json', 'text/html']) return best == 'text/html' and \ request.accept_mimetypes[best] > \ request.accept_mimetypes['application/json'] - - @staticmethod - def get_variables(variables): - if variables and isinstance(variables, six.text_type): - try: - return json.loads(variables) - except: - raise HttpQueryError(400, 'Variables are invalid JSON.') - return variables - - @classmethod - def get_graphql_params(cls, data): - query = data.get('query') - variables = cls.get_variables(data.get('variables')) - id = data.get('id') - operation_name = data.get('operationName') - - return GraphQLParams(query, variables, operation_name, id) - - @staticmethod - def format_error(error): - if isinstance(error, GraphQLError): - return format_graphql_error(error) - - return {'message': six.text_type(error)} diff --git a/tests/app.py b/tests/app.py index 9f11aee..bdecf53 100644 --- a/tests/app.py +++ b/tests/app.py @@ -1,6 +1,6 @@ from flask import Flask from flask_graphql import GraphQLView -from .schema import Schema +from schema import Schema def create_app(path='/graphql', **kwargs): From 3404fad307d357bcbc11db2ca0d9839eff5d706f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 19 Mar 2017 17:16:09 -0700 Subject: [PATCH 23/47] Improved GraphQL server --- flask_graphql/graphqlview.py | 4 +- graphql_server/__init__.py | 206 +++++++++++++++++++++++++++++++++++ graphql_server/error.py | 7 ++ 3 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 graphql_server/__init__.py create mode 100644 graphql_server/error.py diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 1c438e5..0bebfad 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -59,8 +59,6 @@ def dispatch_request(self): try: request_method = request.method.lower() data = self.parse_body() - if isinstance(data, dict): - data = dict(data, **request.args.to_dict()) show_graphiql = request_method == 'get' and self.should_display_graphiql() catch = HttpQueryError if show_graphiql else None @@ -71,9 +69,9 @@ def dispatch_request(self): self.schema, request_method, data, + query_data=request.args.to_dict(), batch_enabled=self.batch, catch=catch, - # Execute options root_value=self.get_root_value(), context_value=self.get_context(), diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py new file mode 100644 index 0000000..f83ea01 --- /dev/null +++ b/graphql_server/__init__.py @@ -0,0 +1,206 @@ +import json +from collections import namedtuple + +import six +from promise import Promise +from graphql import Source, execute, parse, validate +from graphql.error import format_error as format_graphql_error +from graphql.error import GraphQLError +from graphql.execution import ExecutionResult +from graphql.type.schema import GraphQLSchema +from graphql.utils.get_operation_ast import get_operation_ast + + +from .error import HttpQueryError + + +class SkipException(Exception): + pass + + +GraphQLParams = namedtuple('GraphQLParams', 'query,variables,operation_name,id') +GraphQLResponse = namedtuple('GraphQLResponse', 'result,status_code') + + +def default_format_error(error): + if isinstance(error, GraphQLError): + return format_graphql_error(error) + + return {'message': six.text_type(error)} + + + +def run_http_query(schema, request_method, data, query_data=None, batch_enabled=False, format_error=None, catch=None, **execute_options): + if request_method not in ('get', 'post'): + raise HttpQueryError( + 405, + 'GraphQL only supports GET and POST requests.', + headers={ + 'Allow': 'GET, POST' + } + ) + + is_batch = isinstance(data, list) + + is_get_request = request_method == 'get' + allow_only_query = is_get_request + + if not is_batch: + if not isinstance(data, dict): + raise HttpQueryError( + 400, + 'GraphQL params should be a dict. Received {}.'.format(data) + ) + data = [data] + elif not batch_enabled: + raise HttpQueryError( + 400, + 'Batch GraphQL requests are not enabled.' + ) + + if not data: + raise HttpQueryError( + 400, + 'Received an empty list in the batch request.' + ) + + extra_data = {} + # If is a batch request, we don't consume the data from the query + if not is_batch: + extra_data = query_data + + all_params = [get_graphql_params(entry, extra_data) for entry in data] + + if format_error is None: + format_error = default_format_error + + responses = [format_execution_result(get_response( + schema, + params, + catch, + allow_only_query, + **execute_options + ), params.id, format_error) for params in all_params] + + response, status_codes = zip(*responses) + status_code = max(status_codes) + + if not is_batch: + response = response[0] + + return response, status_code, all_params + + +def load_json_variables(variables): + if variables and isinstance(variables, six.text_type): + try: + return json.loads(variables) + except: + raise HttpQueryError(400, 'Variables are invalid JSON.') + return variables + + +def get_graphql_params(data, query_data): + query = data.get('query') or query_data.get('query') + variables = data.get('variables') or query_data.get('variables') + id = data.get('id') + operation_name = data.get('operationName') or query_data.get('operationName') + + return GraphQLParams(query, load_json_variables(variables), operation_name, id) + + +def get_response(schema, params, catch=None, allow_only_query=False, **kwargs): + if catch is None: + catch = SkipException + try: + execution_result = execute_graphql_request( + schema, + params, + allow_only_query, + **kwargs + ) + except catch: + execution_result = ExecutionResult( + data=None, + invalid=True, + ) + # return GraphQLResponse(None, 400) + + return execution_result + + +def format_execution_result(execution_result, id, format_error): + status_code = 200 + + if isinstance(execution_result, Promise): + execution_result = execution_result.get() + + if execution_result: + response = {} + + if execution_result.errors: + response['errors'] = [format_error(e) for e in execution_result.errors] + + if execution_result.invalid: + status_code = 400 + else: + status_code = 200 + response['data'] = execution_result.data + + if id: + response['id'] = id + + else: + response = None + + return GraphQLResponse(response, status_code) + + +def execute_graphql_request(schema, params, allow_only_query=False, **kwargs): + if not params.query: + raise HttpQueryError(400, 'Must provide query string.') + + try: + source = Source(params.query, name='GraphQL request') + ast = parse(source) + validation_errors = validate(schema, ast) + if validation_errors: + return ExecutionResult( + errors=validation_errors, + invalid=True, + ) + except Exception as e: + return ExecutionResult(errors=[e], invalid=True) + + if allow_only_query: + operation_ast = get_operation_ast(ast, params.operation_name) + if operation_ast and operation_ast.operation != 'query': + raise HttpQueryError( + 405, + 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation), + headers={ + 'Allow': ['POST'], + } + ) + + try: + return execute( + schema, + ast, + operation_name=params.operation_name, + variable_values=params.variables, + **kwargs + ) + + except Exception as e: + return ExecutionResult(errors=[e], invalid=True) + + +def load_json_body(data): + try: + return json.loads(data) + except: + raise HttpQueryError( + 400, + 'POST body sent invalid JSON.' + ) diff --git a/graphql_server/error.py b/graphql_server/error.py new file mode 100644 index 0000000..f9459b7 --- /dev/null +++ b/graphql_server/error.py @@ -0,0 +1,7 @@ +class HttpQueryError(Exception): + def __init__(self, status_code, message=None, is_graphql_error=False, headers=None): + self.status_code = status_code + self.message = message + self.is_graphql_error = is_graphql_error + self.headers = headers + super(HttpQueryError, self).__init__(message) From 15fe6c52037b6acbb7c628b1c920af811b9d01d0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 19 Mar 2017 17:25:37 -0700 Subject: [PATCH 24/47] Improved format execution_results --- flask_graphql/graphqlview.py | 20 +++++++++++++++----- graphql_server/__init__.py | 22 +++++----------------- tests/test_graphqlview.py | 15 +++++++++------ 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 0bebfad..174ddf0 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -4,7 +4,7 @@ from flask.views import View from graphql.type.schema import GraphQLSchema -from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body +from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body, format_execution_result from .render_graphiql import render_graphiql @@ -65,11 +65,11 @@ def dispatch_request(self): pretty = self.pretty or show_graphiql or request.args.get('pretty') - result, status_code, all_params = run_http_query( + execution_results, all_params = run_http_query( self.schema, request_method, data, - query_data=request.args.to_dict(), + query_data=request.args, batch_enabled=self.batch, catch=catch, # Execute options @@ -78,6 +78,16 @@ def dispatch_request(self): middleware=self.get_middleware(), executor=self.get_executor(), ) + responses = [ + format_execution_result(execution_result, default_format_error) + for execution_result in execution_results + ] + result, status_codes = zip(*responses) + status_code = max(status_codes) + + # If is not batch + if not isinstance(data, list): + result = result[0] result = self.json_encode(result, pretty) @@ -110,14 +120,14 @@ def parse_body(self): # information provided by content_type content_type = request.mimetype if content_type == 'application/graphql': - return {'query': request.data.decode()} + return {'query': request.data.decode('utf8')} elif content_type == 'application/json': return load_json_body(request.data.decode('utf8')) elif content_type == 'application/x-www-form-urlencoded' \ or content_type == 'multipart/form-data': - return request.form.to_dict() + return request.form return {} diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index f83ea01..f66c15a 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -30,7 +30,7 @@ def default_format_error(error): -def run_http_query(schema, request_method, data, query_data=None, batch_enabled=False, format_error=None, catch=None, **execute_options): +def run_http_query(schema, request_method, data, query_data=None, batch_enabled=False, catch=None, **execute_options): if request_method not in ('get', 'post'): raise HttpQueryError( 405, @@ -71,24 +71,15 @@ def run_http_query(schema, request_method, data, query_data=None, batch_enabled= all_params = [get_graphql_params(entry, extra_data) for entry in data] - if format_error is None: - format_error = default_format_error - - responses = [format_execution_result(get_response( + responses = [get_response( schema, params, catch, allow_only_query, **execute_options - ), params.id, format_error) for params in all_params] - - response, status_codes = zip(*responses) - status_code = max(status_codes) + ) for params in all_params] - if not is_batch: - response = response[0] - - return response, status_code, all_params + return responses, all_params def load_json_variables(variables): @@ -129,7 +120,7 @@ def get_response(schema, params, catch=None, allow_only_query=False, **kwargs): return execution_result -def format_execution_result(execution_result, id, format_error): +def format_execution_result(execution_result, format_error): status_code = 200 if isinstance(execution_result, Promise): @@ -147,9 +138,6 @@ def format_execution_result(execution_result, id, format_error): status_code = 200 response['data'] = execution_result.data - if id: - response['id'] = id - else: response = None diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 9697358..8461d3c 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -470,13 +470,16 @@ def test_post_multipart_data(client): def test_batch_allows_post_with_json_encoding(client): response = client.post( url_string(), - data=jl(id=1, query='{test}'), + data=jl( + # id=1, + query='{test}' + ), content_type='application/json' ) assert response.status_code == 200 assert response_json(response) == [{ - 'id': 1, + # 'id': 1, 'data': {'test': "Hello World"} }] @@ -486,7 +489,7 @@ def test_batch_supports_post_json_query_with_json_variables(client): response = client.post( url_string(), data=jl( - id=1, + # id=1, query='query helloWho($who: String){ test(who: $who) }', variables={'who': "Dolly"} ), @@ -495,7 +498,7 @@ def test_batch_supports_post_json_query_with_json_variables(client): assert response.status_code == 200 assert response_json(response) == [{ - 'id': 1, + # 'id': 1, 'data': {'test': "Hello Dolly"} }] @@ -505,7 +508,7 @@ def test_batch_allows_post_with_operation_name(client): response = client.post( url_string(), data=jl( - id=1, + # id=1, query=''' query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } @@ -521,7 +524,7 @@ def test_batch_allows_post_with_operation_name(client): assert response.status_code == 200 assert response_json(response) == [{ - 'id': 1, + # 'id': 1, 'data': { 'test': 'Hello World', 'shared': 'Hello Everyone' From c1b22d7d738ade88770d5f865d80e00b4abcc678 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Mar 2017 00:50:30 -0700 Subject: [PATCH 25/47] Improved GraphQL server integration --- flask_graphql/graphqlview.py | 41 ++++++++++++------------------------ graphql_server/__init__.py | 38 ++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 174ddf0..9c64a6e 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -1,10 +1,11 @@ import json +from functools import partial from flask import Response, request from flask.views import View from graphql.type.schema import GraphQLSchema -from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body, format_execution_result +from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body, encode_execution_results, json_encode from .render_graphiql import render_graphiql @@ -54,8 +55,10 @@ def render_graphiql(self, params, result): graphiql_template=self.graphiql_template, ) + format_error = staticmethod(default_format_error) + encode = staticmethod(json_encode) + def dispatch_request(self): - try: request_method = request.method.lower() data = self.parse_body() @@ -72,24 +75,19 @@ def dispatch_request(self): query_data=request.args, batch_enabled=self.batch, catch=catch, + # Execute options root_value=self.get_root_value(), context_value=self.get_context(), middleware=self.get_middleware(), executor=self.get_executor(), ) - responses = [ - format_execution_result(execution_result, default_format_error) - for execution_result in execution_results - ] - result, status_codes = zip(*responses) - status_code = max(status_codes) - - # If is not batch - if not isinstance(data, list): - result = result[0] - - result = self.json_encode(result, pretty) + result, status_code = encode_execution_results( + execution_results, + is_batch=isinstance(data, list), + format_error=self.format_error, + encode=partial(self.encode, pretty=pretty) + ) if show_graphiql: return self.render_graphiql( @@ -105,8 +103,8 @@ def dispatch_request(self): except HttpQueryError as e: return Response( - self.json_encode({ - 'errors': [default_format_error(e)] + self.encode({ + 'errors': [self.format_error(e)] }), status=e.status_code, headers=e.headers, @@ -131,17 +129,6 @@ def parse_body(self): return {} - @staticmethod - def json_encode(data, pretty=False): - if not pretty: - return json.dumps(data, separators=(',', ':')) - - return json.dumps( - data, - indent=2, - separators=(',', ': ') - ) - def should_display_graphiql(self): if not self.graphiql or 'raw' in request.args: return False diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index f66c15a..1e29aba 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -67,7 +67,7 @@ def run_http_query(schema, request_method, data, query_data=None, batch_enabled= extra_data = {} # If is a batch request, we don't consume the data from the query if not is_batch: - extra_data = query_data + extra_data = query_data or {} all_params = [get_graphql_params(entry, extra_data) for entry in data] @@ -82,6 +82,31 @@ def run_http_query(schema, request_method, data, query_data=None, batch_enabled= return responses, all_params +def encode_execution_results(execution_results, format_error, is_batch, encode): + responses = [ + format_execution_result(execution_result, format_error) + for execution_result in execution_results + ] + result, status_codes = zip(*responses) + status_code = max(status_codes) + + if not is_batch: + result = result[0] + + return encode(result), status_code + + +def json_encode(data, pretty=False): + if not pretty: + return json.dumps(data, separators=(',', ':')) + + return json.dumps( + data, + indent=2, + separators=(',', ': ') + ) + + def load_json_variables(variables): if variables and isinstance(variables, six.text_type): try: @@ -111,21 +136,14 @@ def get_response(schema, params, catch=None, allow_only_query=False, **kwargs): **kwargs ) except catch: - execution_result = ExecutionResult( - data=None, - invalid=True, - ) - # return GraphQLResponse(None, 400) - + return None + return execution_result def format_execution_result(execution_result, format_error): status_code = 200 - if isinstance(execution_result, Promise): - execution_result = execution_result.get() - if execution_result: response = {} From 12c3e3040005700307907548b1a92574d7a744c5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Mar 2017 00:57:37 -0700 Subject: [PATCH 26/47] Fixed issues --- flask_graphql/graphqlview.py | 8 ++++---- flask_graphql/render_graphiql.py | 1 - tests/app.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index 9c64a6e..038938f 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -1,11 +1,12 @@ -import json from functools import partial from flask import Response, request from flask.views import View from graphql.type.schema import GraphQLSchema -from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body, encode_execution_results, json_encode +from graphql_server import (HttpQueryError, default_format_error, + encode_execution_results, json_encode, + load_json_body, run_http_query) from .render_graphiql import render_graphiql @@ -123,8 +124,7 @@ def parse_body(self): elif content_type == 'application/json': return load_json_body(request.data.decode('utf8')) - elif content_type == 'application/x-www-form-urlencoded' \ - or content_type == 'multipart/form-data': + elif content_type in ('application/x-www-form-urlencoded', 'multipart/form-data'): return request.form return {} diff --git a/flask_graphql/render_graphiql.py b/flask_graphql/render_graphiql.py index bd2f714..1ecfe8a 100644 --- a/flask_graphql/render_graphiql.py +++ b/flask_graphql/render_graphiql.py @@ -1,6 +1,5 @@ from flask import render_template_string - GRAPHIQL_VERSION = '0.7.1' TEMPLATE = ''' - - - - {{graphiql_html_title|default("GraphiQL", true)}} - - - - - - - - - - - -''' - - -def render_graphiql(params, result, graphiql_version=None, - graphiql_template=None, graphiql_html_title=None): - graphiql_version = graphiql_version or GRAPHIQL_VERSION - template = graphiql_template or TEMPLATE - - return render_template_string( - template, - graphiql_version=graphiql_version, - graphiql_html_title=graphiql_html_title, - result=result, - params=params - ) diff --git a/setup.cfg b/setup.cfg index 34e51b5..b6ff204 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [flake8] exclude = tests,scripts,setup.py,docs -max-line-length = 160 +max-line-length = 88 [isort] known_first_party=graphql [tool:pytest] -norecursedirs = venv .tox .cache +norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache diff --git a/setup.py b/setup.py index a851be7..9a7c1cc 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,28 @@ from setuptools import setup, find_packages install_requires = [ - "flask>=0.7.0", - "graphql-core>=2.3,<3", - "graphql-server-core>=1.1,<2", + "graphql-server>=3.0.0b1", ] tests_requires = [ - 'pytest>=2.7.2', - 'pytest-cov==2.8.1', - 'pytest-flask>=0.10.0', + "pytest>=5.4,<5.5", + "pytest-cov>=2.8,<3", ] dev_requires = [ - 'flake8==3.7.9', - 'isort<4.0.0', - 'check-manifest>=0.40,<1', + "flake8>=3.7,<4", + "isort>=4,<5", + "check-manifest>=0.40,<1", ] + tests_requires +with open("README.md", encoding="utf-8") as readme_file: + readme = readme_file.read() + setup( name="Flask-GraphQL", version="2.0.1", description="Adds GraphQL support to your Flask application", - long_description=open("README.md").read(), + long_description=readme, long_description_content_type="text/markdown", url="https://github.com/graphql-python/flask-graphql", download_url="https://github.com/graphql-python/flask-graphql/releases", @@ -33,13 +33,9 @@ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: 3.8", "License :: OSI Approved :: MIT License", ], keywords="api graphql protocol rest flask", diff --git a/tests/app.py b/tests/app.py index 0d38502..84f1d23 100644 --- a/tests/app.py +++ b/tests/app.py @@ -1,19 +1,18 @@ from flask import Flask + from flask_graphql import GraphQLView -from .schema import Schema -from graphql import GraphQLCachedBackend -# from quiver.backend import GraphQLQuiverBackend +from tests.schema import Schema -def create_app(path='/graphql', **kwargs): - # backend = GraphQLCachedBackend(GraphQLQuiverBackend({"async_framework": "PROMISE"})) - backend = None +def create_app(path="/graphql", **kwargs): app = Flask(__name__) app.debug = True - app.add_url_rule(path, view_func=GraphQLView.as_view('graphql', schema=Schema, backend=backend, **kwargs)) + app.add_url_rule( + path, view_func=GraphQLView.as_view("graphql", schema=Schema, **kwargs) + ) return app -if __name__ == '__main__': +if __name__ == "__main__": app = create_app(graphiql=True) app.run() diff --git a/tests/schema.py b/tests/schema.py index f841672..fdb5c9a 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -1,4 +1,5 @@ -from graphql.type.definition import GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType +from graphql.type.definition import (GraphQLArgument, GraphQLField, + GraphQLNonNull, GraphQLObjectType) from graphql.type.scalars import GraphQLString from graphql.type.schema import GraphQLSchema @@ -8,31 +9,39 @@ def resolve_raises(*_): QueryRootType = GraphQLObjectType( - name='QueryRoot', + name="QueryRoot", fields={ - 'thrower': GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_raises), - 'request': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: info.context.args.get('q')), - 'context': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: info.context), - 'test': GraphQLField( - type=GraphQLString, - args={ - 'who': GraphQLArgument(GraphQLString) - }, - resolver=lambda obj, info, who='World': 'Hello %s' % who - ) - } + "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_raises), + "request": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"].args.get("q"), + ), + "context": GraphQLField( + GraphQLObjectType( + name="context", + fields={ + "session": GraphQLField(GraphQLString), + "request": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"], + ), + }, + ), + resolve=lambda obj, info: info.context, + ), + "test": GraphQLField( + type_=GraphQLString, + args={"who": GraphQLArgument(GraphQLString)}, + resolve=lambda obj, info, who="World": "Hello %s" % who, + ), + }, ) MutationRootType = GraphQLObjectType( - name='MutationRoot', + name="MutationRoot", fields={ - 'writeTest': GraphQLField( - type=QueryRootType, - resolver=lambda *_: QueryRootType - ) - } + "writeTest": GraphQLField(type_=QueryRootType, resolve=lambda *_: QueryRootType) + }, ) Schema = GraphQLSchema(QueryRootType, MutationRootType) diff --git a/tests/test_graphiqlview.py b/tests/test_graphiqlview.py index aa92202..4a55710 100644 --- a/tests/test_graphiqlview.py +++ b/tests/test_graphiqlview.py @@ -1,7 +1,7 @@ import pytest +from flask import url_for from .app import create_app -from flask import url_for @pytest.fixture @@ -22,33 +22,39 @@ def client(app): def test_graphiql_is_enabled(app, client): with app.test_request_context(): - response = client.get(url_for('graphql', externals=False), headers={'Accept': 'text/html'}) + response = client.get( + url_for("graphql", externals=False), headers={"Accept": "text/html"} + ) assert response.status_code == 200 def test_graphiql_renders_pretty(app, client): with app.test_request_context(): - response = client.get(url_for('graphql', query='{test}'), headers={'Accept': 'text/html'}) + response = client.get( + url_for("graphql", query="{test}"), headers={"Accept": "text/html"} + ) assert response.status_code == 200 pretty_response = ( - '{\n' + "{\n" ' "data": {\n' ' "test": "Hello World"\n' - ' }\n' - '}' - ).replace("\"", "\\\"").replace("\n", "\\n") + " }\n" + "}".replace('"', '\\"').replace("\n", "\\n") + ) - assert pretty_response in response.data.decode('utf-8') + assert pretty_response in response.data.decode("utf-8") def test_graphiql_default_title(app, client): with app.test_request_context(): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) - assert 'GraphiQL' in response.data.decode('utf-8') + response = client.get(url_for("graphql"), headers={"Accept": "text/html"}) + assert "GraphiQL" in response.data.decode("utf-8") -@pytest.mark.parametrize('app', [create_app(graphiql=True, graphiql_html_title="Awesome")]) +@pytest.mark.parametrize( + "app", [create_app(graphiql=True, graphiql_html_title="Awesome")] +) def test_graphiql_custom_title(app, client): with app.test_request_context(): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) - assert 'Awesome' in response.data.decode('utf-8') + response = client.get(url_for("graphql"), headers={"Accept": "text/html"}) + assert "Awesome" in response.data.decode("utf-8") diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 3e49e55..961a8e0 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -1,18 +1,11 @@ -import pytest import json +from io import StringIO +from urllib.parse import urlencode -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode +import pytest +from flask import url_for from .app import create_app -from flask import url_for @pytest.fixture @@ -33,10 +26,10 @@ def client(app): def url_string(app, **url_params): with app.test_request_context(): - string = url_for('graphql') + string = url_for("graphql") if url_params: - string += '?' + urlencode(url_params) + string += "?" + urlencode(url_params) return string @@ -54,357 +47,354 @@ def json_dump_kwarg_list(**kwargs): def test_allows_get_with_query_param(app, client): - response = client.get(url_string(app, query='{test}')) + response = client.get(url_string(app, query="{test}")) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_get_with_variable_values(app, client): - response = client.get(url_string( - app, - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - )) + response = client.get( + url_string( + app, + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ) + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_allows_get_with_operation_name(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - operationName='helloWorld' - )) + """, + operationName="helloWorld", + ) + ) assert response.status_code == 200 assert response_json(response) == { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } + "data": {"test": "Hello World", "shared": "Hello Everyone"} } def test_reports_validation_errors(app, client): - response = client.get(url_string( - app, - query='{ test, unknownOne, unknownTwo }' - )) + response = client.get(url_string(app, query="{ test, unknownOne, unknownTwo }")) assert response.status_code == 400 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Cannot query field "unknownOne" on type "QueryRoot".', - 'locations': [{'line': 1, 'column': 9}] + "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", + "locations": [{"line": 1, "column": 9}], + "path": None, }, { - 'message': 'Cannot query field "unknownTwo" on type "QueryRoot".', - 'locations': [{'line': 1, 'column': 21}] - } + "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", + "locations": [{"line": 1, "column": 21}], + "path": None, + }, ] } def test_errors_when_missing_operation_name(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } - ''' - )) + """, + ) + ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Must provide operation name if query contains multiple operations.' + "message": "Must provide operation name if query contains multiple operations.", # noqa: E501 + "locations": None, + "path": None, } ] } def test_errors_when_sending_a_mutation_via_get(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" mutation TestMutation { writeTest { test } } - ''' - )) + """, + ) + ) assert response.status_code == 405 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Can only perform a mutation operation from a POST request.' + "message": "Can only perform a mutation operation from a POST request.", + "locations": None, + "path": None, } ] } def test_errors_when_selecting_a_mutation_within_a_get(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } - ''', - operationName='TestMutation' - )) + """, + operationName="TestMutation", + ) + ) assert response.status_code == 405 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Can only perform a mutation operation from a POST request.' + "message": "Can only perform a mutation operation from a POST request.", + "locations": None, + "path": None, } ] } def test_allows_mutation_to_exist_within_a_get(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } - ''', - operationName='TestQuery' - )) + """, + operationName="TestQuery", + ) + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_post_with_json_encoding(app, client): - response = client.post(url_string(app), data=json_dump_kwarg(query='{test}'), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg(query="{test}"), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_sending_a_mutation_via_post(app, client): - response = client.post(url_string(app), data=json_dump_kwarg(query='mutation TestMutation { writeTest { test } }'), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg(query="mutation TestMutation { writeTest { test } }"), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'writeTest': {'test': 'Hello World'}} - } + assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} def test_allows_post_with_url_encoding(app, client): - response = client.post(url_string(app), data=urlencode(dict(query='{test}')), content_type='application/x-www-form-urlencoded') + response = client.post( + url_string(app), + data=urlencode(dict(query="{test}")), + content_type="application/x-www-form-urlencoded", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } - - -# def test_benchmark(client, benchmark): -# url = url_string() -# data = urlencode(dict(query='{test}')) -# def fun(): -# return client.post(url_string(), data=data, content_type='application/x-www-form-urlencoded') - -# response = benchmark(fun) -# assert response.status_code == 200 -# assert response_json(response) == { -# 'data': {'test': "Hello World"} -# } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_supports_post_json_query_with_string_variables(app, client): - response = client.post(url_string(app), data=json_dump_kwarg( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - ), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_json_query_with_json_variables(app, client): - response = client.post(url_string(app), data=json_dump_kwarg( - query='query helloWho($who: String){ test(who: $who) }', - variables={'who': "Dolly"} - ), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + variables={"who": "Dolly"}, + ), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_url_encoded_query_with_string_variables(app, client): - response = client.post(url_string(app), data=urlencode(dict( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - )), content_type='application/x-www-form-urlencoded') + response = client.post( + url_string(app), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ) + ), + content_type="application/x-www-form-urlencoded", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_json_quey_with_get_variable_values(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), data=json_dump_kwarg( - query='query helloWho($who: String){ test(who: $who) }', - ), content_type='application/json') + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_post_url_encoded_query_with_get_variable_values(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), data=urlencode(dict( - query='query helloWho($who: String){ test(who: $who) }', - )), content_type='application/x-www-form-urlencoded') + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + content_type="application/x-www-form-urlencoded", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_raw_text_query_with_get_variable_values(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), - data='query helloWho($who: String){ test(who: $who) }', - content_type='application/graphql' + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data="query helloWho($who: String){ test(who: $who) }", + content_type="application/graphql", ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_allows_post_with_operation_name(app, client): - response = client.post(url_string(app), data=json_dump_kwarg( - query=''' + response = client.post( + url_string(app), + data=json_dump_kwarg( + query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - operationName='helloWorld' - ), content_type='application/json') + """, + operationName="helloWorld", + ), + content_type="application/json", + ) assert response.status_code == 200 assert response_json(response) == { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } + "data": {"test": "Hello World", "shared": "Hello Everyone"} } def test_allows_post_with_get_operation_name(app, client): - response = client.post(url_string( - app, - operationName='helloWorld' - ), data=''' + response = client.post( + url_string(app, operationName="helloWorld"), + data=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - content_type='application/graphql') + """, + content_type="application/graphql", + ) assert response.status_code == 200 assert response_json(response) == { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } + "data": {"test": "Hello World", "shared": "Hello Everyone"} } -@pytest.mark.parametrize('app', [create_app(pretty=True)]) +@pytest.mark.parametrize("app", [create_app(pretty=True)]) def test_supports_pretty_printing(app, client): - response = client.get(url_string(app, query='{test}')) + response = client.get(url_string(app, query="{test}")) assert response.data.decode() == ( - '{\n' - ' "data": {\n' - ' "test": "Hello World"\n' - ' }\n' - '}' + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) -@pytest.mark.parametrize('app', [create_app(pretty=False)]) +@pytest.mark.parametrize("app", [create_app(pretty=False)]) def test_not_pretty_by_default(app, client): - response = client.get(url_string(app, query='{test}')) + response = client.get(url_string(app, query="{test}")) - assert response.data.decode() == ( - '{"data":{"test":"Hello World"}}' - ) + assert response.data.decode() == '{"data":{"test":"Hello World"}}' def test_supports_pretty_printing_by_request(app, client): - response = client.get(url_string(app, query='{test}', pretty='1')) + response = client.get(url_string(app, query="{test}", pretty="1")) assert response.data.decode() == ( - '{\n' - ' "data": {\n' - ' "test": "Hello World"\n' - ' }\n' - '}' + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) def test_handles_field_errors_caught_by_graphql(app, client): - response = client.get(url_string(app, query='{thrower}')) + response = client.get(url_string(app, query="{thrower}")) assert response.status_code == 200 assert response_json(response) == { - 'data': None, - 'errors': [{'locations': [{'column': 2, 'line': 1}], 'path': ['thrower'], 'message': 'Throws!'}] + "errors": [ + { + "locations": [{"column": 2, "line": 1}], + "path": ["thrower"], + "message": "Throws!", + } + ], + "data": None, } def test_handles_syntax_errors_caught_by_graphql(app, client): - response = client.get(url_string(app, query='syntaxerror')) + response = client.get(url_string(app, query="syntaxerror")) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'locations': [{'column': 1, 'line': 1}], - 'message': 'Syntax Error GraphQL (1:1) ' - 'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n'}] + "errors": [ + { + "locations": [{"column": 1, "line": 1}], + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None, + } + ] } @@ -413,162 +403,196 @@ def test_handles_errors_caused_by_a_lack_of_query(app, client): assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Must provide query string.'}] + "errors": [ + {"message": "Must provide query string.", "locations": None, "path": None} + ] } def test_handles_batch_correctly_if_is_disabled(app, client): - response = client.post(url_string(app), data='[]', content_type='application/json') + response = client.post(url_string(app), data="[]", content_type="application/json") assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Batch GraphQL requests are not enabled.'}] + "errors": [ + { + "message": "Batch GraphQL requests are not enabled.", + "locations": None, + "path": None, + } + ] } def test_handles_incomplete_json_bodies(app, client): - response = client.post(url_string(app), data='{"query":', content_type='application/json') + response = client.post( + url_string(app), data='{"query":', content_type="application/json" + ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'POST body sent invalid JSON.'}] + "errors": [ + {"message": "POST body sent invalid JSON.", "locations": None, "path": None} + ] } def test_handles_plain_post_text(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), - data='query helloWho($who: String){ test(who: $who) }', - content_type='text/plain' + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data="query helloWho($who: String){ test(who: $who) }", + content_type="text/plain", ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Must provide query string.'}] + "errors": [ + {"message": "Must provide query string.", "locations": None, "path": None} + ] } def test_handles_poorly_formed_variables(app, client): - response = client.get(url_string( - app, - query='query helloWho($who: String){ test(who: $who) }', - variables='who:You' - )) + response = client.get( + url_string( + app, + query="query helloWho($who: String){ test(who: $who) }", + variables="who:You", + ) + ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Variables are invalid JSON.'}] + "errors": [ + {"message": "Variables are invalid JSON.", "locations": None, "path": None} + ] } def test_handles_unsupported_http_methods(app, client): - response = client.put(url_string(app, query='{test}')) + response = client.put(url_string(app, query="{test}")) assert response.status_code == 405 - assert response.headers['Allow'] in ['GET, POST', 'HEAD, GET, POST, OPTIONS'] + assert response.headers["Allow"] in ["GET, POST", "HEAD, GET, POST, OPTIONS"] assert response_json(response) == { - 'errors': [{'message': 'GraphQL only supports GET and POST requests.'}] + "errors": [ + { + "message": "GraphQL only supports GET and POST requests.", + "locations": None, + "path": None, + } + ] } def test_passes_request_into_request_context(app, client): - response = client.get(url_string(app, query='{request}', q='testing')) + response = client.get(url_string(app, query="{request}", q="testing")) assert response.status_code == 200 - assert response_json(response) == { - 'data': { - 'request': 'testing' - } - } + assert response_json(response) == {"data": {"request": "testing"}} -@pytest.mark.parametrize('app', [create_app(get_context_value=lambda:"CUSTOM CONTEXT")]) +@pytest.mark.parametrize("app", [create_app(context={"session": "CUSTOM CONTEXT"})]) def test_passes_custom_context_into_context(app, client): - response = client.get(url_string(app, query='{context}')) + response = client.get(url_string(app, query="{context { session request }}")) assert response.status_code == 200 - assert response_json(response) == { - 'data': { - 'context': 'CUSTOM CONTEXT' - } - } + res = response_json(response) + assert "data" in res + assert "session" in res["data"]["context"] + assert "request" in res["data"]["context"] + assert "CUSTOM CONTEXT" in res["data"]["context"]["session"] + assert "Request" in res["data"]["context"]["request"] + + +@pytest.mark.parametrize("app", [create_app(context="CUSTOM CONTEXT")]) +def test_context_remapped_if_not_mapping(app, client): + response = client.get(url_string(app, query="{context { session request }}")) + + assert response.status_code == 200 + res = response_json(response) + assert "data" in res + assert "session" in res["data"]["context"] + assert "request" in res["data"]["context"] + assert "CUSTOM CONTEXT" not in res["data"]["context"]["request"] + assert "Request" in res["data"]["context"]["request"] def test_post_multipart_data(app, client): - query = 'mutation TestMutation { writeTest { test } }' + query = "mutation TestMutation { writeTest { test } }" response = client.post( url_string(app), - data={ - 'query': query, - 'file': (StringIO(), 'text1.txt'), - }, - content_type='multipart/form-data' + data={"query": query, "file": (StringIO(), "text1.txt")}, + content_type="multipart/form-data", ) assert response.status_code == 200 - assert response_json(response) == {'data': {u'writeTest': {u'test': u'Hello World'}}} + assert response_json(response) == { + "data": {u"writeTest": {u"test": u"Hello World"}} + } -@pytest.mark.parametrize('app', [create_app(batch=True)]) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_allows_post_with_json_encoding(app, client): response = client.post( url_string(app), data=json_dump_kwarg_list( # id=1, - query='{test}' + query="{test}" ), - content_type='application/json' + content_type="application/json", ) assert response.status_code == 200 - assert response_json(response) == [{ - # 'id': 1, - 'data': {'test': "Hello World"} - }] + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello World"} + } + ] -@pytest.mark.parametrize('app', [create_app(batch=True)]) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_supports_post_json_query_with_json_variables(app, client): response = client.post( url_string(app), data=json_dump_kwarg_list( # id=1, - query='query helloWho($who: String){ test(who: $who) }', - variables={'who': "Dolly"} + query="query helloWho($who: String){ test(who: $who) }", + variables={"who": "Dolly"}, ), - content_type='application/json' + content_type="application/json", ) assert response.status_code == 200 - assert response_json(response) == [{ - # 'id': 1, - 'data': {'test': "Hello Dolly"} - }] + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello Dolly"} + } + ] -@pytest.mark.parametrize('app', [create_app(batch=True)]) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_allows_post_with_operation_name(app, client): response = client.post( url_string(app), data=json_dump_kwarg_list( # id=1, - query=''' + query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - operationName='helloWorld' + """, + operationName="helloWorld", ), - content_type='application/json' + content_type="application/json", ) assert response.status_code == 200 - assert response_json(response) == [{ - # 'id': 1, - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello World", "shared": "Hello Everyone"} } - }] + ] diff --git a/tox.ini b/tox.ini index fb4b51e..72ab365 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,35,36,37} + py{36,37,38} flake8,import-order,manifest ; requires = tox-conda @@ -8,25 +8,25 @@ envlist = passenv = * setenv = PYTHONPATH = {toxinidir} -install_command = python -m pip install --ignore-installed {opts} {packages} +install_command = python -m pip install --pre --ignore-installed {opts} {packages} deps = -e.[test] commands = - pytest --cov=flask_graphql tests {posargs} + pytest tests --cov-report=term-missing --cov=flask_graphql {posargs} [testenv:flake8] -basepython=python3.6 +basepython=python3.8 deps = -e.[dev] commands = - flake8 flask_graphql + flake8 setup.py flask_graphql tests [testenv:import-order] -basepython=python3.6 +basepython=python3.8 deps = -e.[dev] commands = - isort --check-only flask_graphql/ -rc + isort -rc flask_graphql/ tests/ [testenv:manifest] -basepython = python3.6 +basepython = python3.8 deps = -e.[dev] commands = check-manifest -v \ No newline at end of file From dfd863dd14902d91d9cc4427b60eb337ee64134c Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 11:31:41 -0500 Subject: [PATCH 45/47] docs: update readme with v3-beta notes --- README.md | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index af4e1ac..7670f48 100644 --- a/README.md +++ b/README.md @@ -21,26 +21,58 @@ Adds GraphQL support to your Flask application. Just use the `GraphQLView` view from `flask_graphql` ```python +from flask import Flask from flask_graphql import GraphQLView -app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)) +from schema import schema + +app = Flask(__name__) + +app.add_url_rule('/graphql', view_func=GraphQLView.as_view( + 'graphql', + schema=schema, + graphiql=True, +)) # Optional, for adding batch query support (used in Apollo-Client) -app.add_url_rule('/graphql/batch', view_func=GraphQLView.as_view('graphql', schema=schema, batch=True)) +app.add_url_rule('/graphql/batch', view_func=GraphQLView.as_view( + 'graphql', + schema=schema, + batch=True +)) + +if __name__ == '__main__': + app.run() ``` -This will add `/graphql` and `/graphiql` endpoints to your app. +This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. + +### Special Note for Graphene v3 + +If you are using the `Schema` type of [Graphene](https://github.com/graphql-python/graphene) library, be sure to use the `graphql_schema` attribute to pass as schema on the `GraphQLView` view. Otherwise, the `GraphQLSchema` from `graphql-core` is the way to go. + +More info at [Graphene v3 release notes](https://github.com/graphql-python/graphene/wiki/v3-release-notes#graphene-schema-no-longer-subclasses-graphqlschema-type) and [GraphQL-core 3 usage](https://github.com/graphql-python/graphql-core#usage). + + +### Supported options for GraphQLView -### Supported options * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. - * `context`: A value to pass as the `context` to the `graphql()` function. - * `root_value`: The `root_value` you want to provide to `executor.execute`. + * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. + * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. - * `executor`: The `Executor` that you want to use to execute queries. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). + * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. + * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). + * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). + * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. + * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws. + * `headers`: An optional GraphQL string to use as the initial displayed request headers, if not provided, the stored headers will be used. + * `default_query`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If not provided, GraphiQL will use its own default query. +* `header_editor_enabled`: An optional boolean which enables the header editor when true. Defaults to **false**. +* `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**. You can also subclass `GraphQLView` and overwrite `get_root_value(self, request)` to have a dynamic root value per request. From d1ac3a56d3fe6b5eb9c78e9786a3ffd4b1b9e633 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 11:40:39 -0500 Subject: [PATCH 46/47] chore: add flask as extra on install requires --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a7c1cc..d8a626e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages install_requires = [ - "graphql-server>=3.0.0b1", + "graphql-server[flask]>=3.0.0b1", ] tests_requires = [ From 2f627ed4028f7dbe986cbbe5a89486822a2c0b83 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 12:18:55 -0500 Subject: [PATCH 47/47] docs: update contribute link --- CONTRIBUTING.md | 94 ------------------------------------------------- MANIFEST.in | 1 - README.md | 2 +- 3 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index db93994..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,94 +0,0 @@ -# Contributing - -Thanks for helping to make flask-graphql awesome! - -We welcome all kinds of contributions: - -- Bug fixes -- Documentation improvements -- New features -- Refactoring & tidying - - -## Getting started - -If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/flask-graphql/issues) and [pull requests](https://github.com/graphql-python/flask-graphql/pulls) in progress - someone could already be working on something similar and you can help out. - - -## Project setup - -### Development with virtualenv (recommended) - -After cloning this repo, create a virtualenv: - -```console -virtualenv flask-graphql-dev -``` - -Activate the virtualenv and install dependencies by running: - -```console -python pip install -e ".[test]" -``` - -If you are using Linux or MacOS, you can make use of Makefile command -`make dev-setup`, which is a shortcut for the above python command. - -### Development on Conda - -You must create a new env (e.g. `flask-graphql-dev`) with the following command: - -```sh -conda create -n flask-graphql-dev python=3.8 -``` - -Then activate the environment with `conda activate flask-graphql-dev`. - -Proceed to install all dependencies by running: - -```console -python pip install -e ".[test]" -``` - -And you ready to start development! - -## Running tests - -After developing, the full test suite can be evaluated by running: - -```sh -pytest tests --cov=flask_graphql -vv -``` - -If you are using Linux or MacOS, you can make use of Makefile command -`make tests`, which is a shortcut for the above python command. - -You can also test on several python environments by using tox. - -### Running tox on virtualenv - -Install tox: - -```console -pip install tox -``` - -Run `tox` on your virtualenv (do not forget to activate it!) -and that's it! - -### Running tox on Conda - -In order to run `tox` command on conda, install -[tox-conda](https://github.com/tox-dev/tox-conda): - -```sh -conda install -c conda-forge tox-conda -``` - -This install tox underneath so no need to install it before. - -Then uncomment the `requires = tox-conda` line on `tox.ini` file. - -Run `tox` and you will see all the environments being created -and all passing tests. :rocket: - diff --git a/MANIFEST.in b/MANIFEST.in index 5854051..0fa13a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include LICENSE include README.md -include CONTRIBUTING.md include tox.ini include Makefile diff --git a/README.md b/README.md index 7670f48..3546b1d 100644 --- a/README.md +++ b/README.md @@ -85,4 +85,4 @@ class UserRootValue(GraphQLView): ``` ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) +Since v3, `flask-graphql` code lives at [graphql-server](https://github.com/graphql-python/graphql-server) repository to keep any breaking change on the base package on sync with all other integrations. In order to contribute, please take a look at [CONTRIBUTING.md](https://github.com/graphql-python/graphql-server/blob/master/CONTRIBUTING.md).