From 0f00e6762be272b69eb0e0cd6c0c6af230ac00b3 Mon Sep 17 00:00:00 2001 From: karol-gruszczyk Date: Fri, 23 Mar 2018 11:39:05 -0700 Subject: [PATCH] add Operation wrapper --- slothql/__init__.py | 4 +- slothql/django/types/model.py | 2 +- slothql/django/utils/request.py | 16 +++---- slothql/django/utils/tests/request.py | 48 ++++++++----------- slothql/django/views.py | 18 +++---- slothql/operation.py | 35 ++++++++++++++ slothql/query.py | 36 ++++++++++---- .../tests/query.py => tests/operation.py} | 12 ++--- slothql/types/enum.py | 2 +- slothql/types/union.py | 2 +- slothql/utils/__init__.py | 2 - slothql/utils/query.py | 15 ------ 12 files changed, 112 insertions(+), 80 deletions(-) create mode 100644 slothql/operation.py rename slothql/{utils/tests/query.py => tests/operation.py} (71%) delete mode 100644 slothql/utils/query.py diff --git a/slothql/__init__.py b/slothql/__init__.py index 8b5fb1d..31b16ac 100644 --- a/slothql/__init__.py +++ b/slothql/__init__.py @@ -3,6 +3,7 @@ from .types.fields import Field, Integer, Float, String, Boolean, ID, JsonString, DateTime, Date, Time from .types import BaseType, Object, Enum, EnumValue, Union from .schema import Schema +from .operation import Operation from .query import gql __all__ = ( @@ -21,13 +22,14 @@ 'Enum', 'EnumValue', 'Union', + 'Operation', 'Schema', 'gql', ) try: from slothql import django - __all__ += 'django', + __all__ += ('django',) except ImportError as e: if str(e) != "No module named 'django'": raise diff --git a/slothql/django/types/model.py b/slothql/django/types/model.py index e8a7bf2..a4da0a1 100644 --- a/slothql/django/types/model.py +++ b/slothql/django/types/model.py @@ -12,7 +12,7 @@ class ModelOptions(ObjectOptions): - __slots__ = 'model', + __slots__ = ('model',) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/slothql/django/utils/request.py b/slothql/django/utils/request.py index 242b48d..743aaba 100644 --- a/slothql/django/utils/request.py +++ b/slothql/django/utils/request.py @@ -1,23 +1,21 @@ from django.core.exceptions import ValidationError from django.core.handlers.wsgi import WSGIRequest -from slothql import utils +from slothql.operation import Operation -def get_query_from_request(request: WSGIRequest) -> str: +def get_operation_from_request(request: WSGIRequest) -> Operation: if request.method == 'GET': - return request.GET.get('query', '') + return Operation.from_dict(request.GET) if not request.content_type: raise ValidationError('content-type not specified') - if request.content_type == 'application/graphql': - return request.body.decode() - elif request.content_type == 'application/json': + if request.content_type in ('application/graphql', 'application/json', 'multipart/form-data'): try: - return utils.query_from_raw_json(request.body.decode('utf-8')) + return Operation.from_string(request.body.decode()) except ValueError as e: raise ValidationError(str(e)) - elif request.content_type in ('application/x-www-form-urlencoded', 'multipart/form-data'): - return request.POST.get('query', '') + elif request.content_type in ('application/x-www-form-urlencoded',): + return Operation.from_dict(request.POST) raise ValidationError(f'Unsupported content-type {request.content_type}') diff --git a/slothql/django/utils/tests/request.py b/slothql/django/utils/tests/request.py index e23cdc3..ea94a7b 100644 --- a/slothql/django/utils/tests/request.py +++ b/slothql/django/utils/tests/request.py @@ -5,49 +5,43 @@ from unittest import mock from django.core.exceptions import ValidationError +from django.core.handlers.wsgi import WSGIRequest from django.test import RequestFactory -from ..request import get_query_from_request +from ..request import get_operation_from_request query = 'query { hello }' rf = RequestFactory() -def test_query_from_request__get(): - assert get_query_from_request(rf.get('', data={'query': query})) == query - - -def test_query_from_request__post_x_www_form_url_encoded(): - content_type = 'application/x-www-form-urlencoded' - assert query == get_query_from_request(rf.post('', data=urlencode({'query': query}), content_type=content_type)) - - -def test_query_from_request__post_multipart(): - assert query == get_query_from_request(rf.post('', data={'query': query})) +@pytest.mark.parametrize('value', ( + rf.get('', data={'query': query}), + rf.post('', data=json.dumps({'query': query}), content_type='multipart/form-data'), + rf.post('', data=urlencode({'query': query}), content_type='application/x-www-form-urlencoded'), + rf.post('', data=json.dumps({'query': query}), content_type='application/graphql'), + rf.post('', data=json.dumps({'query': query}), content_type='application/json'), +)) +def test_query_from_request__get(value: WSGIRequest): + assert query == get_operation_from_request(value).query def test_query_from_request__post_no_content_type(): with pytest.raises(ValidationError) as exc_info: - assert query == get_query_from_request(rf.post('', data={'query': query}, content_type='')) + assert query == get_operation_from_request(rf.post('', data={'query': query}, content_type='')).query assert 'content-type not specified' in str(exc_info.value) -def test_query_from_request__post_unsupported(): +@pytest.mark.parametrize('content_type', ( + 'text/html', +)) +def test_query_from_request__post_unsupported(content_type): with pytest.raises(ValidationError) as exc_info: - assert query == get_query_from_request(rf.post('', data={'query': query}, content_type='text/html')) - assert 'Unsupported content-type text/html' in str(exc_info.value) - - -def test_query_from_request__post_graphql(): - assert query == get_query_from_request(rf.post('', data=query, content_type='application/graphql')) - - -def test_query_from_request__post_json(): - assert get_query_from_request(rf.post('', data=json.dumps({'query': query}), content_type='application/json')) + assert query == get_operation_from_request(rf.post('', data={'query': query}, content_type=content_type)).query + assert f'Unsupported content-type {content_type}' in str(exc_info.value) -@mock.patch('slothql.utils.query_from_raw_json', side_effect=ValueError('mocked exception')) -def test_query_from_request__post_invalid_json(query_from_raw_json): +@mock.patch('slothql.operation.from_raw_json', side_effect=ValueError('mocked exception')) +def test_query_from_request__post_invalid_json(from_raw_json): with pytest.raises(ValidationError) as exc_info: - get_query_from_request(rf.post('', data='{"query": "hello"}', content_type='application/json')) + get_operation_from_request(rf.post('', data='{"query": "hello"}', content_type='application/json')) assert 'mocked exception' in str(exc_info.value) diff --git a/slothql/django/views.py b/slothql/django/views.py index 3ac1540..2f2e3fa 100644 --- a/slothql/django/views.py +++ b/slothql/django/views.py @@ -12,10 +12,10 @@ from graphql.type.schema import GraphQLSchema -from slothql import gql +import slothql from slothql.template import get_template_string -from .utils.request import get_query_from_request +from .utils.request import get_operation_from_request class GraphQLView(View): @@ -32,7 +32,7 @@ def dispatch(self, request: WSGIRequest, *args, **kwargs) -> HttpResponse: return HttpResponseNotAllowed(['GET', 'POST'], 'GraphQL supports only GET and POST requests.') try: - query = self.get_query() + query = self.get_operation() except ValidationError as e: return HttpResponseBadRequest(e.message) @@ -45,14 +45,14 @@ def dispatch(self, request: WSGIRequest, *args, **kwargs) -> HttpResponse: return HttpResponse(content=result, status=status_code, content_type='application/json') - def get_query(self) -> str: - return get_query_from_request(self.request) + def get_operation(self) -> slothql.Operation: + return get_operation_from_request(self.request) - def execute_query(self, query: str) -> dict: - return gql(self.get_schema(), query) + def execute_operation(self, operation: slothql.Operation) -> dict: + return slothql.gql(schema=self.get_schema(), operation=operation) - def get_query_result(self, query: str) -> Tuple[Optional[str], int]: - result = self.execute_query(query) + def get_query_result(self, operation: slothql.Operation) -> Tuple[Optional[str], int]: + result = self.execute_operation(operation) return self.jsonify(result), 400 if 'errors' in result else 200 @classmethod diff --git a/slothql/operation.py b/slothql/operation.py new file mode 100644 index 0000000..9df828e --- /dev/null +++ b/slothql/operation.py @@ -0,0 +1,35 @@ +import json + +from typing import NamedTuple + + +def from_raw_json(data: str) -> dict: + try: + json_query = json.loads(data) + except json.JSONDecodeError as e: + raise ValueError(str(e)) + + if not isinstance(json_query, dict): + raise ValueError('GraphQL queries must a dictionary') + + if 'query' not in json_query: + raise ValueError('"query" not found in json object') + return json_query + + +class Operation(NamedTuple): + query: str + variables: dict + operation_name: str + + @classmethod + def from_string(cls, string: str) -> 'class': + return cls.from_dict(from_raw_json(string)) + + @classmethod + def from_dict(cls, data: dict): + return cls( + query=data.get('query'), + variables=data.get('variables'), + operation_name=data.get('operationName') or data.get('operation_name'), + ) diff --git a/slothql/query.py b/slothql/query.py index 18bbbd3..b58735f 100644 --- a/slothql/query.py +++ b/slothql/query.py @@ -29,14 +29,14 @@ def middleware(resolver, obj, info, **kwargs): class Query: - __slots__ = 'query', 'schema', 'variables', 'ast', 'errors' + __slots__ = 'schema', 'operation', 'ast', 'errors' - def __init__(self, query: str, schema: slothql.Schema, variables: dict = None): - assert isinstance(query, str), f'Expected query string, got {query}' + def __init__(self, schema: slothql.Schema, operation: slothql.Operation): + assert isinstance(operation.query, str), f'Expected query string, got {operation.query}' assert isinstance(schema, GraphQLSchema), f'schema has to be of type Schema, not {schema}' - self.query, self.schema, self.variables = query, schema, variables or {} - self.ast, self.errors = self.get_ast(self.query, self.schema) + self.schema, self.operation = schema, operation + self.ast, self.errors = self.get_ast(self.operation.query, self.schema) @classmethod def get_ast(cls, query, schema) -> Tuple[Any, List[GraphQLSyntaxError]]: @@ -49,7 +49,13 @@ def get_ast(cls, query, schema) -> Tuple[Any, List[GraphQLSyntaxError]]: def execute(self) -> ExecutionResult: if self.errors: return ExecutionResult(errors=[self.format_error(e) for e in self.errors]) - result = execute(self.schema, self.ast, middleware=[middleware]) + result = execute( + schema=self.schema, + document_ast=self.ast, + variable_values=self.operation.variables, + operation_name=self.operation.operation_name, + middleware=[middleware], + ) return ExecutionResult(data=result.data) @classmethod @@ -59,5 +65,19 @@ def format_error(cls, error: Exception) -> dict: return {'message': str(error)} -def gql(schema: slothql.Schema, query: str, variables: dict = None) -> ExecutionResult: - return Query(query, schema, variables).execute() +def gql( + schema: slothql.Schema, + query: str = None, + *, + variables: dict = None, + operation_name: str = None, + operation: slothql.Operation = None, +) -> ExecutionResult: + if operation: + assert not query, 'Using "query" with "operation" is ambiguous' + assert not variables, 'Using "variables" with "operation" is ambiguous' + assert not operation_name, 'Using "operation_name" with "operation" is ambiguous' + else: + operation = slothql.Operation(query=query, variables=variables, operation_name=operation_name) + + return Query(schema, operation).execute() diff --git a/slothql/utils/tests/query.py b/slothql/tests/operation.py similarity index 71% rename from slothql/utils/tests/query.py rename to slothql/tests/operation.py index 4267246..cf459b2 100644 --- a/slothql/utils/tests/query.py +++ b/slothql/tests/operation.py @@ -1,6 +1,6 @@ import pytest -from ..query import query_from_raw_json +from slothql.operation import Operation @pytest.mark.parametrize('query', ( @@ -9,7 +9,7 @@ )) def test_raw_query_invalid_json(query): with pytest.raises(ValueError) as exc_info: - query_from_raw_json(query) + Operation.from_string(query) assert str(exc_info.value).startswith('Expecting ') @@ -22,15 +22,15 @@ def test_raw_query_invalid_json(query): )) def test_raw_query__valid_json_invalid_type(query): with pytest.raises(ValueError) as exc_info: - query_from_raw_json(query) + Operation.from_string(query) assert str(exc_info.value) == 'GraphQL queries must a dictionary' def test_raw_query__valid_json_missing_query(): with pytest.raises(ValueError) as exc_info: - query_from_raw_json('{"lol": "elo"}') - assert str(exc_info.value) == '"query" not not found in json object' + Operation.from_string('{"lol": "elo"}') + assert str(exc_info.value) == '"query" not found in json object' def test_raw_query__valid_query(): - query_from_raw_json('{"query": "elo"}') + Operation.from_string('{"query": "elo"}') diff --git a/slothql/types/enum.py b/slothql/types/enum.py index f8fc84a..816d0dc 100644 --- a/slothql/types/enum.py +++ b/slothql/types/enum.py @@ -4,7 +4,7 @@ class EnumOptions(BaseOptions): - __slots__ = 'enum_values', + __slots__ = ('enum_values',) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/slothql/types/union.py b/slothql/types/union.py index d602659..c39d8b8 100644 --- a/slothql/types/union.py +++ b/slothql/types/union.py @@ -7,7 +7,7 @@ class UnionOptions(BaseOptions): - __slots__ = 'union_types', + __slots__ = ('union_types',) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/slothql/utils/__init__.py b/slothql/utils/__init__.py index dd35d65..32ddf65 100644 --- a/slothql/utils/__init__.py +++ b/slothql/utils/__init__.py @@ -1,11 +1,9 @@ from .attr import get_attr_fields, is_magic_name, get_attrs from .case import snake_to_camelcase -from .query import query_from_raw_json from .laziness import LazyInitMixin __all__ = ( 'get_attr_fields', 'is_magic_name', 'get_attrs', 'snake_to_camelcase', - 'query_from_raw_json', 'LazyInitMixin', ) diff --git a/slothql/utils/query.py b/slothql/utils/query.py deleted file mode 100644 index 44207ff..0000000 --- a/slothql/utils/query.py +++ /dev/null @@ -1,15 +0,0 @@ -import json - - -def query_from_raw_json(data: str) -> str: - try: - json_query = json.loads(data) - except json.JSONDecodeError as e: - raise ValueError(str(e)) - - if not isinstance(json_query, dict): - raise ValueError('GraphQL queries must a dictionary') - - if 'query' not in json_query: - raise ValueError('"query" not not found in json object') - return json_query['query']