Skip to content

Commit

Permalink
add Operation wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
karol-gruszczyk committed Mar 23, 2018
1 parent 4d22e9d commit 0f00e67
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 80 deletions.
4 changes: 3 additions & 1 deletion slothql/__init__.py
Expand Up @@ -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__ = (
Expand 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
2 changes: 1 addition & 1 deletion slothql/django/types/model.py
Expand Up @@ -12,7 +12,7 @@


class ModelOptions(ObjectOptions):
__slots__ = 'model',
__slots__ = ('model',)

def __init__(self, **kwargs):
super().__init__(**kwargs)
Expand Down
16 changes: 7 additions & 9 deletions 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}')
48 changes: 21 additions & 27 deletions slothql/django/utils/tests/request.py
Expand Up @@ -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)
18 changes: 9 additions & 9 deletions slothql/django/views.py
Expand Up @@ -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):
Expand All @@ -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)

Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions 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'),
)
36 changes: 28 additions & 8 deletions slothql/query.py
Expand Up @@ -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]]:
Expand All @@ -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
Expand All @@ -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()
12 changes: 6 additions & 6 deletions slothql/utils/tests/query.py → 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', (
Expand All @@ -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 ')


Expand All @@ -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"}')
2 changes: 1 addition & 1 deletion slothql/types/enum.py
Expand Up @@ -4,7 +4,7 @@


class EnumOptions(BaseOptions):
__slots__ = 'enum_values',
__slots__ = ('enum_values',)

def __init__(self, **kwargs):
super().__init__(**kwargs)
Expand Down
2 changes: 1 addition & 1 deletion slothql/types/union.py
Expand Up @@ -7,7 +7,7 @@


class UnionOptions(BaseOptions):
__slots__ = 'union_types',
__slots__ = ('union_types',)

def __init__(self, **kwargs):
super().__init__(**kwargs)
Expand Down
2 changes: 0 additions & 2 deletions 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',
)
15 changes: 0 additions & 15 deletions slothql/utils/query.py

This file was deleted.

0 comments on commit 0f00e67

Please sign in to comment.